Skip to main content

linuxutils_system/
nsenter.rs

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    /// Target process to get namespaces from
21    #[arg(short = 't', long = "target")]
22    target: Option<u32>,
23
24    /// Enter all namespaces of the target process
25    #[arg(short = 'a', long)]
26    all: bool,
27
28    /// Enter mount namespace
29    #[arg(short = 'm', long, num_args = 0..=1, default_missing_value = "")]
30    mount: Option<String>,
31
32    /// Enter UTS namespace
33    #[arg(short = 'u', long, num_args = 0..=1, default_missing_value = "")]
34    uts: Option<String>,
35
36    /// Enter IPC namespace
37    #[arg(short = 'i', long, num_args = 0..=1, default_missing_value = "")]
38    ipc: Option<String>,
39
40    /// Enter network namespace
41    #[arg(short = 'n', long, num_args = 0..=1, default_missing_value = "")]
42    net: Option<String>,
43
44    /// Enter PID namespace
45    #[arg(short = 'p', long, num_args = 0..=1, default_missing_value = "")]
46    pid: Option<String>,
47
48    /// Enter user namespace
49    #[arg(short = 'U', long, num_args = 0..=1, default_missing_value = "")]
50    user: Option<String>,
51
52    /// Enter cgroup namespace
53    #[arg(short = 'C', long, num_args = 0..=1, default_missing_value = "")]
54    cgroup: Option<String>,
55
56    /// Enter time namespace
57    #[arg(short = 'T', long, num_args = 0..=1, default_missing_value = "")]
58    time: Option<String>,
59
60    /// Don't fork before exec (default is to fork when entering PID namespace)
61    #[arg(short = 'F', long = "no-fork")]
62    no_fork: bool,
63
64    /// Don't modify UID/GID when entering user namespace
65    #[arg(long = "preserve-credentials")]
66    preserve_credentials: bool,
67
68    /// Set root directory
69    #[arg(short = 'r', long = "root", num_args = 0..=1, default_missing_value = "")]
70    root_dir: Option<String>,
71
72    /// Set working directory
73    #[arg(short = 'w', long = "wd", num_args = 0..=1, default_missing_value = "")]
74    work_dir: Option<String>,
75
76    /// Set UID in entered namespace
77    #[arg(short = 'S', long = "setuid")]
78    set_uid: Option<u32>,
79
80    /// Set GID in entered namespace
81    #[arg(short = 'G', long = "setgid")]
82    set_gid: Option<u32>,
83
84    /// Program and arguments to run
85    #[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    // Build list of namespaces to enter
152    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    // Individual namespace flags override --all entries
169    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    // Enter user namespace first (if present) to gain capabilities
201    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    // Enter remaining namespaces
226    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    // Set root/working directory
237    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    // Set UID/GID if specified (and not already done for user namespace)
267    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}