1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use rustix::{
7 fd::AsFd,
8 fs::{self, Mode, OFlags},
9 thread::{LinkNameSpaceType, move_into_link_name_space},
10};
11use std::{
12 env, io,
13 os::unix::process::CommandExt,
14 process::{Command, ExitCode},
15};
16
17#[derive(Parser)]
18#[command(name = "nsenter", about = "Run a program in different namespaces")]
19pub struct Args {
20 #[arg(short = 't', long = "target")]
22 target: Option<u32>,
23
24 #[arg(short = 'a', long)]
26 all: bool,
27
28 #[arg(short = 'm', long, num_args = 0..=1, default_missing_value = "")]
30 mount: Option<String>,
31
32 #[arg(short = 'u', long, num_args = 0..=1, default_missing_value = "")]
34 uts: Option<String>,
35
36 #[arg(short = 'i', long, num_args = 0..=1, default_missing_value = "")]
38 ipc: Option<String>,
39
40 #[arg(short = 'n', long, num_args = 0..=1, default_missing_value = "")]
42 net: Option<String>,
43
44 #[arg(short = 'p', long, num_args = 0..=1, default_missing_value = "")]
46 pid: Option<String>,
47
48 #[arg(short = 'U', long, num_args = 0..=1, default_missing_value = "")]
50 user: Option<String>,
51
52 #[arg(short = 'C', long, num_args = 0..=1, default_missing_value = "")]
54 cgroup: Option<String>,
55
56 #[arg(short = 'T', long, num_args = 0..=1, default_missing_value = "")]
58 time: Option<String>,
59
60 #[arg(short = 'F', long = "no-fork")]
62 no_fork: bool,
63
64 #[arg(long = "preserve-credentials")]
66 preserve_credentials: bool,
67
68 #[arg(short = 'r', long = "root", num_args = 0..=1, default_missing_value = "")]
70 root_dir: Option<String>,
71
72 #[arg(short = 'w', long = "wd", num_args = 0..=1, default_missing_value = "")]
74 work_dir: Option<String>,
75
76 #[arg(short = 'S', long = "setuid")]
78 set_uid: Option<u32>,
79
80 #[arg(short = 'G', long = "setgid")]
82 set_gid: Option<u32>,
83
84 #[arg(trailing_var_arg = true)]
86 command: Vec<String>,
87}
88
89struct NsEntry {
90 path: String,
91 ns_type: LinkNameSpaceType,
92}
93
94const NS_TYPES: &[(&str, LinkNameSpaceType)] = &[
95 ("mnt", LinkNameSpaceType::Mount),
96 ("uts", LinkNameSpaceType::HostNameAndNISDomainName),
97 ("ipc", LinkNameSpaceType::InterProcessCommunication),
98 ("net", LinkNameSpaceType::Network),
99 ("pid", LinkNameSpaceType::ProcessID),
100 ("user", LinkNameSpaceType::User),
101 ("cgroup", LinkNameSpaceType::ControlGroup),
102 ("time", LinkNameSpaceType::Time),
103];
104
105fn ns_path(pid: u32, ns: &str) -> String {
106 format!("/proc/{pid}/ns/{ns}")
107}
108
109fn resolve_ns_file(
110 explicit: &Option<String>,
111 pid: Option<u32>,
112 ns_name: &str,
113) -> Option<String> {
114 match explicit {
115 Some(path) if !path.is_empty() => Some(path.clone()),
116 Some(_) => pid.map(|p| ns_path(p, ns_name)),
117 None => None,
118 }
119}
120
121fn enter_namespace(path: &str, ns_type: LinkNameSpaceType) -> io::Result<()> {
122 let fd = fs::open(path, OFlags::RDONLY, Mode::empty())
123 .map_err(io::Error::from)?;
124 move_into_link_name_space(fd.as_fd(), Some(ns_type))
125 .map_err(io::Error::from)
126}
127
128fn get_shell() -> String {
129 env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
130}
131
132fn do_setuid(uid: u32) -> io::Result<()> {
133 if unsafe { libc::setuid(uid) } != 0 {
134 Err(io::Error::last_os_error())
135 } else {
136 Ok(())
137 }
138}
139
140fn do_setgid(gid: u32) -> io::Result<()> {
141 if unsafe { libc::setgid(gid) } != 0 {
142 Err(io::Error::last_os_error())
143 } else {
144 Ok(())
145 }
146}
147
148pub fn run(args: Args) -> ExitCode {
149 let pid = args.target;
150
151 let mut entries: Vec<NsEntry> = Vec::new();
153
154 if args.all {
155 if let Some(pid) = pid {
156 for &(ns_name, ns_type) in NS_TYPES {
157 let path = ns_path(pid, ns_name);
158 if std::path::Path::new(&path).exists() {
159 entries.push(NsEntry { path, ns_type });
160 }
161 }
162 } else {
163 eprintln!("nsenter: --all requires --target");
164 return ExitCode::FAILURE;
165 }
166 }
167
168 let ns_opts: &[(&Option<String>, &str, LinkNameSpaceType)] = &[
170 (&args.user, "user", LinkNameSpaceType::User),
171 (&args.mount, "mnt", LinkNameSpaceType::Mount),
172 (
173 &args.uts,
174 "uts",
175 LinkNameSpaceType::HostNameAndNISDomainName,
176 ),
177 (
178 &args.ipc,
179 "ipc",
180 LinkNameSpaceType::InterProcessCommunication,
181 ),
182 (&args.net, "net", LinkNameSpaceType::Network),
183 (&args.pid, "pid", LinkNameSpaceType::ProcessID),
184 (&args.cgroup, "cgroup", LinkNameSpaceType::ControlGroup),
185 (&args.time, "time", LinkNameSpaceType::Time),
186 ];
187
188 for &(opt, ns_name, ns_type) in ns_opts {
189 if let Some(path) = resolve_ns_file(opt, pid, ns_name) {
190 entries.retain(|e| e.ns_type != ns_type);
191 entries.push(NsEntry { path, ns_type });
192 }
193 }
194
195 if entries.is_empty() {
196 eprintln!("nsenter: no namespaces specified");
197 return ExitCode::FAILURE;
198 }
199
200 let has_user_ns =
202 entries.iter().any(|e| e.ns_type == LinkNameSpaceType::User);
203 if has_user_ns {
204 let idx = entries
205 .iter()
206 .position(|e| e.ns_type == LinkNameSpaceType::User)
207 .unwrap();
208 let entry = entries.remove(idx);
209 if let Err(e) = enter_namespace(&entry.path, entry.ns_type) {
210 eprintln!(
211 "nsenter: failed to enter user namespace ({}): {e}",
212 entry.path
213 );
214 return ExitCode::FAILURE;
215 }
216
217 if !args.preserve_credentials {
218 let uid = args.set_uid.unwrap_or(0);
219 let gid = args.set_gid.unwrap_or(0);
220 let _ = do_setgid(gid);
221 let _ = do_setuid(uid);
222 }
223 }
224
225 for entry in &entries {
227 if let Err(e) = enter_namespace(&entry.path, entry.ns_type) {
228 eprintln!(
229 "nsenter: failed to enter namespace ({}): {e}",
230 entry.path
231 );
232 return ExitCode::FAILURE;
233 }
234 }
235
236 if let Some(ref dir) = args.root_dir {
238 let dir = if dir.is_empty() {
239 pid.map(|p| format!("/proc/{p}/root"))
240 .unwrap_or_else(|| "/".to_string())
241 } else {
242 dir.clone()
243 };
244 if let Err(e) = std::os::unix::fs::chroot(&dir) {
245 eprintln!("nsenter: chroot to {dir} failed: {e}");
246 return ExitCode::FAILURE;
247 }
248 if let Err(e) = env::set_current_dir("/") {
249 eprintln!("nsenter: chdir failed: {e}");
250 return ExitCode::FAILURE;
251 }
252 }
253 if let Some(ref dir) = args.work_dir {
254 let dir = if dir.is_empty() {
255 pid.map(|p| format!("/proc/{p}/cwd"))
256 .unwrap_or_else(|| ".".to_string())
257 } else {
258 dir.clone()
259 };
260 if let Err(e) = env::set_current_dir(&dir) {
261 eprintln!("nsenter: chdir to {dir} failed: {e}");
262 return ExitCode::FAILURE;
263 }
264 }
265
266 if !has_user_ns {
268 if let Some(gid) = args.set_gid
269 && let Err(e) = do_setgid(gid)
270 {
271 eprintln!("nsenter: setgid failed: {e}");
272 return ExitCode::FAILURE;
273 }
274 if let Some(uid) = args.set_uid
275 && let Err(e) = do_setuid(uid)
276 {
277 eprintln!("nsenter: setuid failed: {e}");
278 return ExitCode::FAILURE;
279 }
280 }
281
282 let program = if args.command.is_empty() {
283 get_shell()
284 } else {
285 args.command[0].clone()
286 };
287 let program_args: Vec<&str> = if args.command.len() > 1 {
288 args.command[1..].iter().map(|s| s.as_str()).collect()
289 } else {
290 vec![]
291 };
292
293 let has_pid_ns = entries
294 .iter()
295 .any(|e| e.ns_type == LinkNameSpaceType::ProcessID);
296
297 if has_pid_ns && !args.no_fork {
298 match Command::new(&program).args(&program_args).status() {
299 Ok(s) => ExitCode::from(s.code().unwrap_or(1) as u8),
300 Err(e) => {
301 eprintln!("nsenter: failed to execute {program}: {e}");
302 ExitCode::FAILURE
303 }
304 }
305 } else {
306 let err = Command::new(&program).args(&program_args).exec();
307 eprintln!("nsenter: failed to execute {program}: {err}");
308 ExitCode::FAILURE
309 }
310}