blob: 9e8034a29330dfdbb33bd75741a9effda3ddf3a7 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
//! Transfer a private-network listener from the trusted pre-exec helper to Host.
//! Only the listener crosses namespaces. The target inherits no Host socket.
use rustix::net::{
RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, ReturnFlags, SendAncillaryBuffer,
SendAncillaryMessage, SendFlags,
};
use std::{
io::{self, IoSlice, IoSliceMut},
mem::MaybeUninit,
net::{Ipv4Addr, TcpListener},
os::{
fd::{AsFd, FromRawFd, OwnedFd, RawFd},
unix::{net::UnixStream, process::CommandExt},
},
path::Path,
time::Duration,
};
pub const HELPER: &str = "__maka-network-namespace";
const DEADLINE: Duration = Duration::from_secs(15);
pub(crate) fn prepare(
network: maka_sandbox::Network,
route: maka_network::Policy,
) -> io::Result<(std::fs::File, maka_network::proxy::Proxy)> {
let (host, child) = UnixStream::pair()?;
host.set_nonblocking(true)?;
let host = tokio::net::UnixStream::from_std(host)?;
let listener = async move {
tokio::time::timeout(DEADLINE, async {
let listener = loop {
host.readable().await?;
match host.try_io(tokio::io::Interest::READABLE, || receive(&host)) {
Err(error) if error.kind() == io::ErrorKind::WouldBlock => continue,
result => break result?,
}
};
listener.set_nonblocking(true)?;
let listener = tokio::net::TcpListener::from_std(listener)?;
// A listener is owned by Host before the helper executes any user
// code. The private channel is dropped on both sides after this ACK.
loop {
host.writable().await?;
match host.try_write(&[1]) {
Ok(1) => break,
Err(error) if error.kind() == io::ErrorKind::WouldBlock => continue,
_ => return Err(io::Error::other("network namespace acknowledgement failed")),
}
}
Ok(listener)
})
.await
.map_err(|_| {
io::Error::new(
io::ErrorKind::TimedOut,
"network namespace handoff timed out",
)
})?
};
Ok((
std::fs::File::from(OwnedFd::from(child)),
maka_network::proxy::Proxy::serve(network, route, listener)?,
))
}
fn receive(channel: &tokio::net::UnixStream) -> io::Result<TcpListener> {
let mut byte = [0];
let mut storage = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
let mut control = RecvAncillaryBuffer::new(&mut storage);
let result = rustix::net::recvmsg(
channel,
&mut [IoSliceMut::new(&mut byte)],
&mut control,
RecvFlags::CMSG_CLOEXEC,
)?;
let mut files = Vec::new();
let mut unexpected = false;
for message in control.drain() {
match message {
RecvAncillaryMessage::ScmRights(rights) => files.extend(rights),
_ => unexpected = true,
}
}
if result.bytes != 1
|| byte != [1]
|| result
.flags
.intersects(ReturnFlags::CTRUNC | ReturnFlags::TRUNC)
|| unexpected
|| files.len() != 1
{
return Err(io::Error::other("invalid network namespace listener"));
}
let listener = TcpListener::from(files.remove(0));
if rustix::net::sockopt::socket_type(&listener)? != rustix::net::SocketType::STREAM
|| rustix::net::sockopt::socket_protocol(&listener)? != Some(rustix::net::ipproto::TCP)
|| !rustix::net::sockopt::socket_acceptconn(&listener)?
|| !matches!(listener.local_addr()?, std::net::SocketAddr::V4(address) if *address.ip() == Ipv4Addr::LOCALHOST && address.port() != 0)
{
return Err(io::Error::other(
"network namespace requires a loopback TCP listener",
));
}
Ok(listener)
}
/// Run only in the single-threaded trusted CLI entry point, after bubblewrap
/// creates the namespaces and before starting the untrusted target.
///
/// # Safety
/// The descriptor is an exclusively owned inherited bootstrap socket, not an
/// existing Rust-owned descriptor. The process must not have other threads.
pub unsafe fn enter(
control: RawFd,
executable: &Path,
args: Vec<std::ffi::OsString>,
) -> io::Result<()> {
if control <= 2 || !executable.is_absolute() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid network namespace bootstrap",
));
}
// SAFETY: this entry point exclusively owns the inherited descriptor.
let mut channel = unsafe { UnixStream::from_raw_fd(control) };
channel.set_read_timeout(Some(DEADLINE))?;
channel.set_write_timeout(Some(DEADLINE))?;
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
let port = listener.local_addr()?.port();
let files = [listener.as_fd()];
let mut storage = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
let mut ancillary = SendAncillaryBuffer::new(&mut storage);
if !ancillary.push(SendAncillaryMessage::ScmRights(&files)) {
return Err(io::Error::other("network namespace descriptor buffer"));
}
let count = rustix::io::retry_on_intr(|| {
rustix::net::sendmsg(
&channel,
&[IoSlice::new(&[1])],
&mut ancillary,
SendFlags::NOSIGNAL,
)
})?;
if count != 1 {
return Err(io::Error::other(
"network namespace listener handoff failed",
));
}
use std::io::Read;
let mut ack = [0];
channel.read_exact(&mut ack)?;
if ack != [1] {
return Err(io::Error::other("network namespace listener refused"));
}
drop(channel);
drop(listener);
// All launcher descriptors are CLOEXEC before the target starts. This is
// single-threaded and does not mutate the multithreaded Host's descriptor table.
if unsafe {
libc::syscall(
libc::SYS_close_range,
3u32,
u32::MAX,
libc::CLOSE_RANGE_CLOEXEC,
)
} < 0
{
let error = io::Error::last_os_error();
if !matches!(error.raw_os_error(), Some(libc::ENOSYS | libc::EINVAL)) {
return Err(error);
}
let descriptors = std::fs::read_dir("/proc/self/fd")?
.map(|entry| {
entry?
.file_name()
.to_str()
.ok_or_else(|| io::Error::other("invalid descriptor filename"))?
.parse::<RawFd>()
.map_err(io::Error::other)
})
.collect::<io::Result<Vec<_>>>()?;
for fd in descriptors.into_iter().filter(|fd| *fd > 2) {
// SAFETY: setting CLOEXEC does not access memory or close the fd;
// the single-threaded helper cannot race another fd allocator.
if unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) } < 0
&& io::Error::last_os_error().raw_os_error() != Some(libc::EBADF)
{
return Err(io::Error::last_os_error());
}
}
}
let mut command = std::process::Command::new(executable);
command.args(args);
let proxy = format!("http://127.0.0.1:{port}");
for key in [
"HTTP_PROXY",
"http_proxy",
"HTTPS_PROXY",
"https_proxy",
"ALL_PROXY",
"all_proxy",
] {
command.env(key, &proxy);
}
command.env("NO_PROXY", "").env("no_proxy", "");
Err(command.exec())
}