1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
|
//! Running Mycelium for this node: either a daemon the operator runs (its REST URL), or
//! one we start and supervise (`SupervisedMycelium`): its keys handed over in memory,
//! never on disk, and it dies with us.
use anyhow::{Context, Result, anyhow, bail};
use std::fs::File;
use std::io::Write;
use std::net::TcpListener;
use std::os::unix::io::FromRawFd;
use std::path::PathBuf;
use std::process::Stdio;
use tokio::process::{Child, Command};
use url::Url;
/// Represents where a 32-byte cryptographic key originates.
#[derive(Debug, Clone)]
pub enum KeySource {
Raw([u8; 32]),
Path(PathBuf),
}
/// Atomic structure ensuring private network credentials are always handled together.
#[derive(Debug, Clone)]
pub struct PrivateNetworkConfig {
pub name: String,
pub key: KeySource,
}
/// Comprehensive parameters required to spawn or target an instance.
#[derive(Debug, Clone)]
pub struct MyceliumSpawnConfig {
pub rest_url_opt: Option<Url>,
pub key_file_opt: Option<PathBuf>,
pub network_key_file: Option<PathBuf>,
/// Set together with `network_key_file` (`Settings::check`).
pub network_name: Option<String>,
pub tcp_port: Option<u16>,
pub peers: Vec<String>,
}
impl Default for MyceliumSpawnConfig {
fn default() -> Self {
Self {
rest_url_opt: None,
key_file_opt: None,
network_key_file: None,
network_name: None,
tcp_port: None,
peers: vec!["tls://127.0.0.1:9651".to_string()],
}
}
}
pub enum MyceliumRuntime {
Supervised(Box<SupervisedMycelium>),
External,
}
pub struct MyceliumSetup {
pub runtime: MyceliumRuntime,
pub rest_url: Url,
}
impl MyceliumSetup {
/// Evaluates the configuration payload and initializes the chosen execution strategy.
pub async fn bootstrap(config: MyceliumSpawnConfig) -> Result<Self> {
// TODO warn for all ignored options if rest_url_opt is set
if let Some(url) = config.rest_url_opt {
tracing::info!(
"Configured to interface with an external standalone Mycelium instance at: {}",
url
);
return Ok(Self {
runtime: MyceliumRuntime::External,
rest_url: url,
});
}
tracing::info!(
"Configured to initialize an embedded user-space Mycelium background worker"
);
let key = match config.key_file_opt {
Some(key_file) => KeySource::Path(key_file),
None => {
let mut bytes = [0u8; 32];
getrandom::fill(&mut bytes)?;
KeySource::Raw(bytes)
}
};
let private_net_config =
config
.network_key_file
.zip(config.network_name)
.map(|(net_key_path, name)| PrivateNetworkConfig {
name,
key: KeySource::Path(net_key_path),
});
let supervised_node =
SupervisedMycelium::spawn(key, private_net_config, config.tcp_port, &config.peers)
.await
.context("Failed to securely bootstrap embedded Mycelium background worker")?;
let resolved_url = Url::parse(&format!(
"http://127.0.0.1:{}",
supervised_node.config.api_port
))?;
Ok(Self {
runtime: MyceliumRuntime::Supervised(Box::new(supervised_node)),
rest_url: resolved_url,
})
}
/// Blocks until the background daemon drops out. If running externally, hangs open indefinitely.
pub async fn wait_for_exit(&mut self) -> std::io::Result<std::process::ExitStatus> {
match &mut self.runtime {
MyceliumRuntime::Supervised(node) => node.wait_for_exit().await,
MyceliumRuntime::External => {
std::future::pending::<std::io::Result<std::process::ExitStatus>>().await
}
}
}
}
#[derive(Debug, Clone)]
pub struct MyceliumNodeConfig {
pub tcp_port: u16,
pub api_port: u16,
pub rpc_port: u16,
pub private_network: Option<String>,
}
pub struct SupervisedMycelium {
child: Child,
pub config: MyceliumNodeConfig,
_key_file_handle: Option<File>,
_net_key_file_handle: Option<File>,
}
impl SupervisedMycelium {
async fn spawn(
node_key: KeySource,
private_network: Option<PrivateNetworkConfig>,
explicit_tcp: Option<u16>,
peers: &[String],
) -> Result<Self> {
// Linux only: keys are handed over in memory (memfd, never on disk), and the
// child dies with us (PR_SET_PDEATHSIG). Elsewhere, run myceliumd yourself.
if cfg!(not(target_os = "linux")) {
bail!(
"Supervising Mycelium is only supported on Linux; run myceliumd yourself \
and pass --mycelium-rest-url"
);
}
let tcp_port = match explicit_tcp {
Some(p) => p,
None => Self::find_available_port().context("Failed to allocate TCP wire port")?,
};
// Local only, so any free port will do.
let api_port = Self::find_available_port().context("Failed to allocate API port")?;
let rpc_port = Self::find_available_port().context("Failed to allocate JSON-RPC port")?;
let config = MyceliumNodeConfig {
tcp_port,
api_port,
rpc_port,
private_network: private_network.as_ref().map(|pn| pn.name.clone()),
};
let parent_pid = std::process::id();
let (key_file_handle, key_file_path, node_fd) =
Self::resolve_key_strategy("mycelium_node_key", node_key, parent_pid)?;
let (net_key_file_handle, net_key_file_path, net_fd) = match private_network {
Some(net_cfg) => {
let (h, p, fd) =
Self::resolve_key_strategy("mycelium_net_key", net_cfg.key, parent_pid)?;
(h, Some(p), fd)
}
None => (None, None, None),
};
let api_addr = format!("127.0.0.1:{}", config.api_port);
let rpc_addr = format!("127.0.0.1:{}", config.rpc_port);
let mut cmd = Command::new("mycelium");
let fds_to_inherit: Vec<i32> = [node_fd, net_fd].iter().filter_map(|&x| x).collect();
// The child must never outlive us. An orphaned Mycelium keeps running with our
// key; the next one we start then shares its address, and replies (including
// Mycelium's own message ACKs) go to whichever instance the routers picked,
// leaving our messages pending forever. Drop only runs on a clean shutdown, so
// let the kernel signal the child when we die, however we die.
let parent_pid = std::process::id() as libc::pid_t;
unsafe {
cmd.pre_exec(move || {
for &fd in &fds_to_inherit {
libc::fcntl(fd, libc::F_SETFD, 0);
}
// Own process group: a Ctrl-C in the terminal goes to continuum only,
// which then stops Mycelium itself, instead of both exiting at once (which
// shows up as "Mycelium runtime process exited").
if libc::setpgid(0, 0) != 0 {
return Err(std::io::Error::last_os_error());
}
#[cfg(target_os = "linux")]
{
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGTERM) != 0 {
return Err(std::io::Error::last_os_error());
}
// We may have died between fork and prctl.
if libc::getppid() != parent_pid {
libc::_exit(1);
}
}
Ok(())
});
}
cmd.arg("--key-file")
.arg(&key_file_path)
.arg("--tcp-listen-port")
.arg(config.tcp_port.to_string())
.arg("--disable-quic")
.arg("--api-addr")
.arg(&api_addr)
.arg("--jsonrpc-addr")
.arg(&rpc_addr)
.arg("--no-tun");
// Peers come from `--mycelium-peers`; no local multicast discovery.
cmd.arg("--disable-peer-discovery");
if let Some(ref net_name) = config.private_network {
cmd.arg("--network-name").arg(net_name);
}
if let Some(ref net_key_path) = net_key_file_path {
cmd.arg("--network-key-file").arg(net_key_path);
}
if !peers.is_empty() {
cmd.arg("--peers").arg(peers.join(" "));
}
cmd.env("RUST_LOG", "info");
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd
.spawn()
.context("Failed to execute mycelium process daemon")?;
// Its output goes where ours does.
if let Some(mut stdout) = child.stdout.take() {
tokio::spawn(async move {
let mut host_stdout = tokio::io::stdout();
let _ = tokio::io::copy(&mut stdout, &mut host_stdout).await;
});
}
if let Some(mut stderr) = child.stderr.take() {
tokio::spawn(async move {
let mut host_stderr = tokio::io::stderr();
let _ = tokio::io::copy(&mut stderr, &mut host_stderr).await;
});
}
let node = Self {
child,
config,
_key_file_handle: key_file_handle,
_net_key_file_handle: net_key_file_handle,
};
Ok(node)
}
fn resolve_key_strategy(
desc_tag: &str,
source: KeySource,
parent_pid: u32,
) -> Result<(Option<File>, PathBuf, Option<i32>)> {
match source {
KeySource::Raw(raw_bytes) => {
let (handle, fd) = Self::create_ephemeral_file(desc_tag, raw_bytes)?;
let pseudo_path = PathBuf::from(format!("/proc/{}/fd/{}", parent_pid, fd));
Ok((Some(handle), pseudo_path, Some(fd)))
}
KeySource::Path(existing_path) => Ok((None, existing_path, None)),
}
}
fn find_available_port() -> Result<u16> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
Ok(addr.port())
}
fn create_ephemeral_file(desc_tag: &str, raw_key: [u8; 32]) -> Result<(File, i32)> {
#[cfg(target_os = "linux")]
{
let memfd_name = std::ffi::CString::new(desc_tag)?;
let fd = unsafe { libc::memfd_create(memfd_name.as_ptr(), 0) };
if fd < 0 {
return Err(anyhow!(
"memfd_create failed natively inside Linux kernel for tag: {}",
desc_tag
));
}
let mut file = unsafe { File::from_raw_fd(fd) };
file.write_all(&raw_key)?;
file.flush()?;
Ok((file, fd))
}
// Unreachable: `spawn` refuses to run elsewhere.
#[cfg(not(target_os = "linux"))]
{
let _ = (desc_tag, raw_key);
bail!("In-memory key files need Linux")
}
}
pub async fn wait_for_exit(&mut self) -> std::io::Result<std::process::ExitStatus> {
self.child.wait().await
}
}
impl Drop for SupervisedMycelium {
fn drop(&mut self) {
// The memfd key files go away with their handles.
let _ = self.child.start_kill();
}
}
|