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
//! System preparation and host utilities.
//!
//! Handles kernel module loading, sysctl configuration, hosts file
//! management, executable lookup in `$PATH`, and container detection.
use crate::{Config, node::Node};
use anyhow::{Context, Result, bail};
use log::{debug, info, warn};
use std::{
env::{split_paths, var, var_os},
fmt::Display,
fs::{self, read_to_string},
net::Ipv4Addr,
path::{Path, PathBuf},
process::Command,
};
/// Manages host-level system preparation and cleanup.
pub struct System {
hosts: Option<String>,
}
impl System {
/// Create a new system
pub fn setup(config: &Config) -> Result<Self> {
if config.is_rootless() {
let in_userns = Self::in_user_namespace();
if !in_userns {
warn!(
"Rootless mode but not inside a user namespace, \
skipping directory setup"
);
} else {
// Inside rootlesskit, pre-existing host directories under the
// copy-up overlay retain host root ownership (mapped to nobody
// in the user namespace) and are not writable. Recreate paths
// that kubernix components write to under rootlesskit copy-ups.
// When adding a new component that writes to a host path under
// /var or /run, add the path here too.
for dir in &[
"/var/lib/kubelet",
"/var/lib/crio",
"/var/lib/containers",
"/var/cache/containers",
"/var/log/pods",
"/var/log/containers",
"/var/log/crio",
"/run/lock",
"/run/containers",
] {
let path = PathBuf::from(dir);
if path.exists()
&& let Err(e) = fs::remove_dir_all(&path)
{
warn!("Unable to remove '{}': {}", dir, e);
}
fs::create_dir_all(&path).with_context(|| {
format!("Unable to create rootless directory '{}'", dir)
})?;
}
fs::create_dir_all("/var/lib/kubelet/device-plugins")
.context("Unable to create /var/lib/kubelet/device-plugins")?;
// Override containers-storage.conf to prevent
// overlay-specific options from leaking into vfs mode.
// Written to the run directory (not /etc/containers/) to
// avoid permission issues inside rootlesskit copy-ups.
let storage_conf = config.root().join("storage.conf");
fs::write(&storage_conf, "[storage]\ndriver = \"vfs\"\n")
.context("Unable to write rootless storage.conf")?;
// SAFETY: called before any threads are spawned.
unsafe { std::env::set_var("CONTAINERS_STORAGE_CONF", &storage_conf) };
// On NixOS, /etc/hosts is a symlink into the read-only nix
// store. Replace it with a regular file so multi-node can
// write host entries.
let hosts_path = Self::hosts();
if hosts_path.is_symlink() {
let content = read_to_string(&hosts_path).unwrap_or_default();
fs::remove_file(&hosts_path).context("Unable to remove hosts symlink")?;
fs::write(&hosts_path, content).context("Unable to write hosts file")?;
}
}
}
if !config.is_rootless() && !Self::in_container()? {
for module in &["overlay", "br_netfilter", "ip_conntrack"] {
Self::modprobe(module)?;
}
for sysctl in &[
"net.bridge.bridge-nf-call-ip6tables",
"net.bridge.bridge-nf-call-iptables",
"net.ipv4.conf.all.route_localnet",
"net.ipv4.ip_forward",
] {
Self::sysctl_enable(sysctl)?;
}
} else {
info!("Skipping modprobe and sysctl (containerized or rootless)");
}
let hosts = if config.multi_node() {
// Try to write the hostnames, which does not work on every system
let hosts_file = Self::hosts();
let hosts = read_to_string(&hosts_file)?;
let local_hosts = (0..config.nodes())
.map(|x| format!("{} {}", Ipv4Addr::LOCALHOST, Node::raw(x)))
.collect::<Vec<_>>();
let mut new_hosts = hosts
.lines()
.filter(|x| !local_hosts.iter().any(|y| x == y))
.map(|x| x.into())
.collect::<Vec<_>>();
new_hosts.extend(local_hosts);
match fs::write(&hosts_file, new_hosts.join("\n")) {
Err(e) => {
debug!(
"Unable to write hosts file '{}'. The nodes may be not reachable: {}",
hosts_file.display(),
e
);
None
}
_ => Some(hosts),
}
} else {
None
};
Ok(Self { hosts })
}
/// Returns true if running inside a user namespace (uid 0 maps to
/// a non-zero host UID). Used to guard destructive host path cleanup
/// and to validate the KUBERNIX_ROOTLESS env var at startup.
///
/// This function is intentionally side-effect-free (no logging)
/// because it may be called before the logger is initialized.
pub(crate) fn in_user_namespace() -> bool {
read_to_string("/proc/self/uid_map")
.map(|s| {
s.lines().any(|line| {
let mut fields = line.split_whitespace();
fields.next() == Some("0")
&& fields.next().is_some_and(|host_uid| host_uid != "0")
})
})
.unwrap_or(false)
}
/// Returns true if the process is running inside a container
pub fn in_container() -> Result<bool> {
Ok(
read_to_string(PathBuf::from("/").join("proc").join("1").join("cgroup"))
.context("Unable to retrieve systems container status")?
.lines()
.any(|x| x.contains("libpod") || x.contains("podman") || x.contains("docker")),
)
}
/// Restore the initial system state
pub fn cleanup(&self) {
if let Some(hosts) = &self.hosts
&& let Err(e) = fs::write(Self::hosts(), hosts)
{
warn!(
"Unable to restore hosts file, may need manual cleanup: {}",
e
);
}
}
/// Find an executable inside the current $PATH environment
pub fn find_executable<P>(name: P) -> Result<PathBuf>
where
P: AsRef<Path> + Display,
{
var_os("PATH")
.and_then(|paths| {
split_paths(&paths)
.filter_map(|dir| {
let full_path = dir.join(&name);
if full_path.is_file() {
Some(full_path)
} else {
None
}
})
.next()
})
.with_context(|| format!("Unable to find executable '{}' in $PATH", name))
}
/// Return the full path to the default system shell
pub fn shell() -> Result<String> {
let shell = var("SHELL").unwrap_or_else(|_| "sh".into());
Ok(format!(
"{}",
Self::find_executable(&shell)
.with_context(|| format!("Unable to find system shell '{}'", shell))?
.display()
))
}
/// Check if a kernel module is already loaded
fn module_loaded(module: &str) -> bool {
read_to_string(PathBuf::from("/proc/modules"))
.map(|content| {
content
.lines()
.any(|l| l.split_whitespace().next() == Some(module))
})
.unwrap_or(false)
}
/// Load a single kernel module via 'modprobe', skipping if already loaded
fn modprobe(module: &str) -> Result<()> {
if Self::module_loaded(module) {
debug!("Kernel module '{}' already loaded, skipping", module);
return Ok(());
}
debug!("Loading kernel module '{}'", module);
let output = Command::new("modprobe").arg(module).output()?;
if !output.status.success() {
bail!(
"Unable to load '{}' kernel module: {}",
module,
String::from_utf8(output.stderr)?,
);
}
Ok(())
}
/// Check if a sysctl is already enabled (value is "1")
fn sysctl_enabled(key: &str) -> bool {
let path = format!("/proc/sys/{}", key.replace('.', "/"));
read_to_string(&path)
.map(|v| v.trim() == "1")
.unwrap_or(false)
}
/// Enable a single sysctl by setting it to '1', skipping if already set
fn sysctl_enable(key: &str) -> Result<()> {
if Self::sysctl_enabled(key) {
debug!("Sysctl '{}' already enabled, skipping", key);
return Ok(());
}
debug!("Enabling sysctl '{}'", key);
let enable_arg = format!("{}=1", key);
let output = Command::new("sysctl").arg("-w").arg(&enable_arg).output()?;
let stderr = String::from_utf8(output.stderr)?;
if !stderr.is_empty() {
bail!("Unable to set sysctl '{}': {}", enable_arg, stderr);
}
Ok(())
}
fn hosts() -> PathBuf {
PathBuf::from("/").join("etc").join("hosts")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::tests::test_config;
const VALID_EXECUTABLE: &str = "echo";
const INVALID_EXECUTABLE: &str = "should-not-exist";
#[test]
fn module_failure() {
assert!(System::modprobe("invalid").is_err());
}
#[test]
fn sysctl_failure() {
assert!(System::sysctl_enable("invalid").is_err());
}
#[test]
fn find_executable_success() {
assert!(System::find_executable(VALID_EXECUTABLE).is_ok());
}
#[test]
fn find_executable_failure() {
assert!(System::find_executable(INVALID_EXECUTABLE).is_err());
}
#[test]
fn find_shell_success() {
temp_env::with_var("SHELL", Some(VALID_EXECUTABLE), || {
assert!(System::shell().is_ok());
});
}
/// Integration test: requires root for modprobe/sysctl.
/// Run with: sudo cargo test -- --ignored
#[test]
#[ignore]
fn setup_success() -> Result<()> {
let c = test_config()?;
let s = System::setup(&c)?;
s.cleanup();
Ok(())
}
}