Skip to content
Snippets Groups Projects
Verified Commit d5916d73 authored by Clara Engler's avatar Clara Engler
Browse files

Implement capability free mode of operation

This commit implements oniux in a capability free fashion by utilizing
the `user_namespaces(7)` feature to gain capabilities on further
namespaces it "owns".  With this, we can performed a privileged
operation, such as the creation of TUN interfaces, without owning any
capabilities.

Many thanks to 7ppKb5bW for teaching on how this can be done, as well as
for providing an initial prototype with this feature enabled.
parent e11f6136
Branches
No related tags found
No related merge requests found
......@@ -415,15 +415,6 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.69.5"
......@@ -754,15 +745,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
......@@ -1762,7 +1744,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.61.0",
"windows-core",
]
[[package]]
......@@ -1840,25 +1822,6 @@ dependencies = [
"rustversion",
]
[[package]]
name = "ipc-channel"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb8251fb7bcd9ccd3725ed8deae9fe7db8e586495c9eb5b0c52e6233e5e75ea"
dependencies = [
"bincode",
"crossbeam-channel",
"fnv",
"lazy_static",
"libc",
"mio",
"rand 0.8.5",
"serde",
"tempfile",
"uuid",
"windows",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
......@@ -2412,13 +2375,14 @@ dependencies = [
"caps",
"clap",
"env_logger",
"ipc-channel",
"log",
"netlink-packet-core",
"netlink-packet-route",
"netlink-sys",
"nix",
"onion-tunnel",
"sendfd",
"smoltcp",
"tempfile",
"tokio",
]
......@@ -3244,6 +3208,15 @@ version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]]
name = "sendfd"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b183bfd5b1bc64ab0c1ef3ee06b008a9ef1b68a7d3a99ba566fbfe7a7c6d745b"
dependencies = [
"libc",
]
[[package]]
name = "serde"
version = "1.0.219"
......@@ -3463,7 +3436,7 @@ checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "smoltcp"
version = "0.10.0"
source = "git+https://gitlab.torproject.org/tpo/core/smoltcp#21751ecc6c918de2e98411956a378ac2d36890a5"
source = "git+https://gitlab.torproject.org/tpo/core/smoltcp.git#21751ecc6c918de2e98411956a378ac2d36890a5"
dependencies = [
"bitflags 1.3.2",
"byteorder",
......@@ -4921,15 +4894,6 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom 0.3.2",
]
[[package]]
name = "valuable"
version = "0.1.1"
......@@ -5106,51 +5070,17 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-implement",
"windows-interface",
"windows-link",
"windows-result 0.3.2",
"windows-strings 0.4.0",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
"windows-result",
"windows-strings",
]
[[package]]
......@@ -5164,17 +5094,6 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
......@@ -5192,15 +5111,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-result"
version = "0.3.2"
......@@ -5210,16 +5120,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result 0.2.0",
"windows-targets",
]
[[package]]
name = "windows-strings"
version = "0.4.0"
......
......
......@@ -9,12 +9,13 @@ anyhow = "1.0.95"
caps = "0.5.5"
clap = { version = "4.5.27", features = ["derive"] }
env_logger = "0.11.6"
ipc-channel = "0.19.0"
log = "0.4.25"
netlink-packet-core = "0.7.0"
netlink-packet-route = "0.22.0"
netlink-sys = "0.8.7"
nix = { version = "0.29.0", features = ["sched", "process", "fs", "mount"] }
nix = { version = "0.29.0", features = ["sched", "process", "fs", "mount", "user"] }
onion-tunnel = { git = "https://gitlab.torproject.org/tpo/core/onionmasq.git" }
sendfd = "0.4.4"
smoltcp = { git = "https://gitlab.torproject.org/tpo/core/smoltcp.git" }
tempfile = "3.19.1"
tokio = { version = "1.44.1", features = ["full"] }
# oniux
**This is still considered experimental software!**
*oniux* is a tool that utilizes various Linux `namespaces(7)` in order to isolate
an arbitrary application over the Tor network. To achieve this, it makes heavy
use of the [onionmasq](https://gitlab.torproject.org/cve/oniux), which offers
a TUN device to send Tor traffic through.
use of the [onionmasq](https://gitlab.torproject.org/tpo/core/onionmasq), which
offers a TUN device to send Tor traffic through.
## Usage
```sh
cargo build
sudo setcap cap_net_admin,cap_sys_admin=ep ./target/debug/oniux
./target/debug/oniux curl https://amiusingtor.net
```
## Internal Workings
*oniux* works by creating a copy of itself in a new PID namespace, so that unexpected
crashes do not leak any zombie processes. After that, the original parent waits
in a `waitpid(2)` until the actual child, which is the PID1 of the PID namespace,
terminates. This PID1 process will from now on be referred to as the "root process".
The root process then `clone(2)` itself and launches an `onion_tunnel` helper process
inside a blocking Tokio runtime. Before, it drops the `CAP_SYS_ADMIN` capability,
as it is no longer required for the operation of an `onion_tunnel`.
Once that is done, `oniux` waits a short amount of time until `onion_tunnel` has setted
up the TUN interface `onion0`, after which it will create the isolation process,
which is later going to be responsible for actually executing the final binary.
The isolation process has its own network and mount namespace.
Upon the creation of the isolation process, the root process then moves the `onion0`
interface into the network namespace of the isolation process. Afterwards, it
sends a short IPC message to the isolation process, signaling that the interface
has been successfully moved.
From there on, the isolation process takes over control. It first configures the
`onion0` interface via netlink (setting it up, adding IPs, ...) and mounts a
custom `/etc/resolv.conf` afterwards, indicating the operating system to perform
DNS resolves only through the onion tunnel resolver listening on `169.254.42.53`.
**TODO**
Once both of these privileged steps have been done, the isolation process clears
all of its permitted capabilities, so that the capabilities of the isolation binary
only depend upon its file capability set.
## Credits
At this point in time, the isolation process `execv(2)`'s the command given to the
program via the command line.
The root process waits until the termination of the isolation process, whose
status will be the exit code of itself.
Last but not least, the original process that has been in a `waitpid(2)` state
since early on returns, returning the status yielded by the root process yielded
by the isolation process.
```mermaid
graph LR
A[Original Process] -- waitpid(2) <--> B
subgraph root[PID & mnt namespace]
subgraph isolation[net & mnt namespace]
C[Isolation Process]
end
B[Root Process] -- waitpid(2) <--> C[Isolation Process]
B --> D[onion_tunnel]
end
```
Many thanks go to `7ppKb5bW`, who taught me on how this can implemented without
the use of `capabilities(7)` by using `user_namespaces(7)` properly.
use std::{
ffi::CString,
io::Write,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
os::{
fd::{AsRawFd, FromRawFd, OwnedFd},
unix::net::UnixDatagram,
},
path::PathBuf,
process::{self},
rc::Rc,
str::FromStr,
process::{Command, ExitCode, ExitStatus},
thread,
time::Duration,
};
use anyhow::{bail, Result};
use caps::{CapSet, Capability, CapsHashSet};
use anyhow::Result;
use caps::CapSet;
use clap::Parser;
use ipc_channel::ipc::IpcReceiver;
use log::{debug, info};
use log::debug;
use netlink_packet_route::AddressFamily;
use nix::{
libc,
sched::{self, CloneFlags},
sys::wait::{self, WaitStatus},
unistd::{self},
unistd::{Gid, Uid},
};
use onion_tunnel::{config::TunnelConfig, scaffolding::LinuxScaffolding, OnionTunnel};
use std::thread;
use sendfd::{RecvWithFd, SendWithFd};
use smoltcp::phy::{Medium, TunTapInterface};
use tempfile::NamedTempFile;
use tokio::runtime::Runtime;
mod mount;
mod netlink;
mod user;
/// The size of the stacks of our child processes
const STACK_SIZE: usize = 1000 * 1000 * 8;
......@@ -42,67 +45,37 @@ struct Args {
cmd: Vec<String>,
}
/// Limit the capabilities of the process in case they were too high
///
/// This function limits the capabilities of the process in case they were too high,
/// by adjusting all capability sets to the bare minimum required for this software
/// to function properly.
///
/// The calling function only needs to ensure that [`Capability::CAP_SYS_ADMIN`]
/// and [`Capability::CAP_NET_ADMIN`] are present in [`CapSet::Permitted`].
///
/// This function panics in case of a failure and double checks its success,
/// because failure here is not tolerable.
///
/// The function should generally be called as soon as possible and only once.
fn limit_caps() {
let sys_net = CapsHashSet::from([Capability::CAP_SYS_ADMIN, Capability::CAP_NET_ADMIN]);
// `Permitted` and `Effective` obviously need `CAP_SYS_ADMIN` and `CAP_NET_ADMIN`,
// because this application and its child processes need to create namespaces
// as well as to move network interfaces between namespaces, alongside various
// other network stack related operations required elevated capabilities.
caps::set(None, CapSet::Permitted, &sys_net).unwrap();
caps::set(None, CapSet::Effective, &sys_net).unwrap();
assert_eq!(caps::read(None, CapSet::Permitted).unwrap(), sys_net);
assert_eq!(caps::read(None, CapSet::Effective).unwrap(), sys_net);
// `Inheritable` and `Ambiet` are in my (cve) opinion too hard to use properly.
caps::clear(None, CapSet::Inheritable).unwrap();
caps::clear(None, CapSet::Ambient).unwrap();
assert_eq!(
caps::read(None, CapSet::Inheritable).unwrap(),
CapsHashSet::new()
);
assert_eq!(
caps::read(None, CapSet::Ambient).unwrap(),
CapsHashSet::new()
);
}
/// Generate an empty stack for calls to `clone(2)`
fn gen_stack() -> Vec<u8> {
vec![0u8; STACK_SIZE]
}
fn isolation(cmd: &[String], rx: Rc<IpcReceiver<u32>>) -> Result<isize> {
fn isolation(parent: UnixDatagram, uid: Uid, gid: Gid, cmd: &[String]) -> Result<ExitStatus> {
// Initialize the mount namespace properly.
mount::init_namespace()?;
debug!("initialized the isolation mount namespace");
mount::procfs(&PathBuf::from("/proc"))?;
debug!("finished mount namespace setup");
// Perform UID and GID mappings.
user::setgroups(false)?;
user::uid_map(uid, uid)?;
user::gid_map(gid, gid)?;
debug!("finished user namespace mappings");
// Overwrite `/etc/resolv.conf` with a bind mound to use the nameservers
// provided by onionmasq.
let mut resolv_conf = NamedTempFile::new()?;
resolv_conf.write_all("nameserver 169.254.42.53\nnameserver fe80::53\n".as_bytes())?;
debug!(
"created temporary resolv.conf(5) at {:?}",
resolv_conf.path()
);
mount::bind(resolv_conf.path(), &PathBuf::from("/etc/resolv.conf"))?;
debug!("mounted {:?} to /etc/resolv.conf", resolv_conf.path());
// Wait until our parent has set up everything nicely
let index = rx.recv()?;
// Configure the IP addresses of the interface
// Create and configure a TUN interface for use with onionmasq.
let tun = TunTapInterface::new(DEVICE_NAME, Medium::Ip)?;
let index = netlink::get_index(DEVICE_NAME)?;
netlink::add_address(index, IpAddr::V4(Ipv4Addr::new(169, 254, 42, 1)), 24)?;
netlink::add_address(
index,
......@@ -114,123 +87,101 @@ fn isolation(cmd: &[String], rx: Rc<IpcReceiver<u32>>) -> Result<isize> {
netlink::set_default_gateway(index, AddressFamily::Inet6)?;
debug!("finished setting up networking");
caps::clear(None, CapSet::Permitted).unwrap();
debug!("cleared all capabilities in isolation process");
let cmd: Vec<CString> = cmd.iter().map(|s| CString::from_str(s).unwrap()).collect();
unistd::execvp(&cmd[0], &cmd)?;
unreachable!()
// Drop all capabilities.
caps::clear(None, CapSet::Permitted)?;
caps::clear(None, CapSet::Effective)?;
caps::clear(None, CapSet::Inheritable)?;
caps::clear(None, CapSet::Ambient)?;
debug!("dropped all capabilites");
// Send the device to the parent.
parent.send_with_fd(&[0; 1024], &[tun.as_raw_fd()])?;
drop(tun);
debug!("sent TUN device");
// The 100ms is a rather arbitrary timeout, but it probably does not hurt
// to wait until the parent has received the file descriptor and launched
// the onion-tunnel thread.
// TODO: Consider using IPC here to indicate that we can continue although
// that might be a little bit overkill.
thread::sleep(Duration::from_millis(100));
// Run the actual child and wait for its termination.
// It is important to not use something like `execve` or anything that else
// that could hinder the execution of Rust Drop traits, as otherwise the
// `resolv_conf` file will leak into the temporary directory.
let mut child = Command::new(&cmd[0]).args(&cmd[1..]).spawn()?;
Ok(child.wait()?)
}
fn onion_tunnel(device: &str) -> Result<isize> {
caps::drop(None, CapSet::Effective, Capability::CAP_SYS_ADMIN)?;
caps::drop(None, CapSet::Permitted, Capability::CAP_SYS_ADMIN)?;
debug!("dropped CAP_SYS_ADMIN in onion tunnel");
let rt = Runtime::new()?;
rt.block_on(async {
let can_mark = LinuxScaffolding::can_mark();
let scaffolding = LinuxScaffolding {
can_mark,
cc: None,
log_connections: false,
};
let mut onion_tunnel =
OnionTunnel::new(scaffolding, device, TunnelConfig::default()).await?;
onion_tunnel.run().await
})?;
unreachable!()
}
fn main_main(args: &Args) -> Result<isize> {
mount::init_namespace()?;
debug!("initialized the main mount namespace");
mount::procfs(&PathBuf::from("/proc"))?;
debug!("mounted procfs in main mount namespace");
let mut onion_tunnel_stack = gen_stack();
let onion_tunnel_proc = unsafe {
sched::clone(
Box::new(|| onion_tunnel(DEVICE_NAME).unwrap()),
&mut onion_tunnel_stack,
CloneFlags::empty(),
None,
)?
};
// TODO: It would be really nice if we could somehow let onion tunnel communicate
// when it is ready, rather than waiting 500ms on a shady trust me basis.
debug!(
"spawned onion tunnel with PID {}",
onion_tunnel_proc.as_raw()
);
thread::sleep(Duration::from_millis(500));
debug!("waited 500ms for onion tunnel to start up");
let index = netlink::get_index(DEVICE_NAME)?;
debug!("found {DEVICE_NAME} interface with index {index}");
let (tx, rx) = ipc_channel::ipc::channel::<u32>()?;
let rx = Rc::new(rx);
let mut isolation_stack = gen_stack();
let isolation_proc = unsafe {
sched::clone(
Box::new(move || isolation(&args.cmd, rx.clone()).unwrap()),
&mut isolation_stack,
CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWNET,
Some(libc::SIGCHLD),
)?
};
debug!("spawned isolation with PID {}", isolation_proc.as_raw());
netlink::set_ns(index, isolation_proc.as_raw() as u32)?;
tx.send(index)?;
// Parent no longer needs capabilities too
caps::clear(None, CapSet::Permitted).unwrap();
debug!("cleared permitted capabilities in main process");
match wait::waitpid(isolation_proc, None)? {
WaitStatus::Exited(_, code) => {
info!("isolated process exited with {code}");
process::exit(code);
}
res => {
info!("isolated process exited with {:?}", res);
process::exit(1);
}
}
}
fn main() -> Result<()> {
fn main() -> Result<ExitCode> {
// Initialize the application.
env_logger::init();
let args = Args::parse();
// Check the capabilities
if !caps::has_cap(None, CapSet::Permitted, Capability::CAP_SYS_ADMIN)? {
bail!("not having CAP_SYS_ADMIN capability");
}
if !caps::has_cap(None, CapSet::Permitted, Capability::CAP_NET_ADMIN)? {
bail!("not having CAP_NET_ADMIN capability");
}
debug!("checked capabilities");
// Create IPC primitives.
let (parent, child) = UnixDatagram::pair()?;
limit_caps();
debug!("limited capabilities");
// Obtain user information.
let uid = Uid::current();
let gid = Gid::current();
let args = Args::parse();
let mut stack = gen_stack();
let proc = unsafe {
sched::clone(
Box::new(|| main_main(&args).unwrap()),
Box::new(|| {
// This statement looks a bit complicated but all it does is
// converting `Result<ExitStatus, Error>` to `isize`.
isolation(parent.try_clone().unwrap(), uid, gid, &args.cmd)
.unwrap()
.code()
.unwrap_or(1)
.try_into()
.unwrap()
}),
&mut stack,
CloneFlags::CLONE_NEWPID | CloneFlags::CLONE_NEWNS,
CloneFlags::CLONE_NEWNET
| CloneFlags::CLONE_NEWNS
| CloneFlags::CLONE_NEWPID
| CloneFlags::CLONE_NEWUSER,
Some(libc::SIGCHLD),
)
}?;
drop(parent);
// Receive file descriptor.
let mut fds = [-1];
let (_, nfds) = child.recv_with_fd(&mut [0; 1024], &mut fds)?;
assert_eq!(nfds, 1);
assert_ne!(fds[0], -1);
let tun = unsafe { OwnedFd::from_raw_fd(fds[0]) };
debug!("received TUN file descriptor");
// Spawn task to handle the TUN device in.
// Maybe we could use `Runtime::spawn` instead, but spawning the task
// ourselves in combinating with `Runtime::block_on` gives me a more fuzzy
// feeling in terms of control.
thread::spawn(|| {
Runtime::new().unwrap().block_on(async move {
let can_mark = LinuxScaffolding::can_mark();
let scaffolding = LinuxScaffolding {
can_mark,
cc: None,
log_connections: false,
};
let mut tunnel = OnionTunnel::create_with_fd(scaffolding, tun, TunnelConfig::default())
.await
.unwrap();
tunnel.run().await
})
});
debug!("spawned onion-tunnel thread");
// Wait until the isolation process `proc` has finished and return its
// status as an `ExitCode`.
match wait::waitpid(proc, None)? {
WaitStatus::Exited(_, code) => process::exit(code),
_ => process::exit(1),
WaitStatus::Exited(_, code) => Ok(ExitCode::from(u8::try_from(code)?)),
_ => Ok(ExitCode::FAILURE),
}
}
......@@ -127,37 +127,6 @@ pub fn set_up(index: u32) -> Result<()> {
Ok(())
}
/// Move the network device to the network namespace of `pid`
pub fn set_ns(index: u32, pid: u32) -> Result<()> {
let mut socket = create_socket(NETLINK_ROUTE)?;
debug!("created netlink socket to move {index} to NS of {pid}");
let mut link_msg = LinkMessage::default();
link_msg.header.index = index;
link_msg.attributes.push(LinkAttribute::NetNsPid(pid));
let mut msg = NetlinkMessage::new(
NetlinkHeader::default(),
NetlinkPayload::from(RouteNetlinkMessage::SetLink(link_msg)),
);
msg.header.flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE;
msg.finalize();
send(&mut socket, &msg)?;
let resp: NetlinkMessage<RouteNetlinkMessage> = recv(&mut socket)?;
// Check for errors (ACK is Error with code zero)
match resp.payload {
NetlinkPayload::Error(ErrorMessage { code: None, .. }) => {}
e => bail!(
"netlink failed for unknown reasons moving {index} to NS of {pid} {:#?}",
e
),
}
debug!("moved interface {index} to NS of {pid}");
Ok(())
}
/// Add `addr` to interface `index`
pub fn add_address(index: u32, addr: IpAddr, prefix_len: u8) -> Result<()> {
let mut socket = create_socket(NETLINK_ROUTE)?;
......
......
//! Helper functions for `user_namespaces(7)`.
//!
//! All functions require a working procfs mount at `/proc`.
use std::{fs::File, io::Write};
use anyhow::Result;
use log::debug;
use nix::unistd::{Gid, Uid};
/// Performs a 1-by-1 mapping of two [`Uid`]'s.
///
/// This function may only be called once per `user_namespaces(7)`.
pub fn uid_map(inner: Uid, outer: Uid) -> Result<()> {
let mut f = File::create("/proc/self/uid_map")?;
f.write(format!("\t{inner}\t{outer}\t1\n").as_bytes())?;
debug!("mapped UID {inner} to {outer}");
Ok(())
}
/// Performs a 1-by-1 mapping of two [`Gid`]'s.
///
/// This function may only be called once per `user_namespaces(7)`.
pub fn gid_map(inner: Gid, outer: Gid) -> Result<()> {
let mut f = File::create("/proc/self/gid_map")?;
f.write(format!("\t{inner}\t{outer}\t1\n").as_bytes())?;
debug!("mapped GID {inner} to {outer}");
Ok(())
}
/// Allow `setgroups(2)` system call in the `user_namespaces(7)`?
///
/// This function may only be called once per `user_namespaces(7)`.
pub fn setgroups(allow: bool) -> Result<()> {
let value = if allow {
"allow\n".as_bytes()
} else {
"deny\n".as_bytes()
};
let mut f = File::create("/proc/self/setgroups")?;
f.write(value)?;
debug!("setgroups {allow}");
Ok(())
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment