rpcs
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing

This commit is contained in:
Jake Hillion 2022-05-03 14:34:26 +01:00
parent 2ef3d46a5b
commit 5b16d240ea
4 changed files with 390 additions and 2 deletions

View File

@ -1,4 +1,6 @@
use super::{Spawner, TriggerData};
use log::info;
use super::{RpcHandler, Spawner, TriggerData};
use crate::specification::{Arg, FileSocket, Pipe};
use crate::void::VoidBuilder;
use crate::{Error, Result};
@ -7,7 +9,10 @@ use std::ffi::CString;
use std::fs::File;
use std::net::TcpListener;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::IntoRawFd;
use std::os::unix::io::{FromRawFd, IntoRawFd};
use nix::sys::socket;
use nix::unistd::{fork, ForkResult};
pub struct PreparedArgs(Vec<PreparedArg>);
@ -89,6 +94,9 @@ enum PreparedArg {
/// A TCP Listener
TcpListener { socket: TcpListener },
/// RPC
Rpc { socket: File },
/// The rest of argv[1..], 0 or more arguments
Trailing,
}
@ -154,6 +162,45 @@ impl PreparedArg {
PreparedArg::TcpListener { socket }
}
Arg::Rpc(specs) => {
let (ambient, void) = socket::socketpair(
socket::AddressFamily::Unix,
socket::SockType::Datagram,
None,
socket::SockFlag::empty(),
)
.map_err(|e| Error::Nix {
msg: "socketpair",
src: e,
})?;
// SAFETY: valid new fd as socketpair(2) returned successfully
let ambient = unsafe { File::from_raw_fd(ambient) };
// SAFETY: valid new fd as socketpair(2) returned successfully
let void = unsafe { File::from_raw_fd(void) };
// spawn this child with ambient authority
// necessary as no void ever has outgoing network capability
// SAFETY: this program is single-threaded so no safety issue
let child = unsafe { fork() }.map_err(|e| Error::Nix {
msg: "fork",
src: e,
})?;
match child {
ForkResult::Child => {
let handler = RpcHandler::new(specs);
handler.handle(ambient).unwrap();
}
ForkResult::Parent { child } => {
info!("spawned rpc handler with pid {}", child);
}
};
// SAFETY: safe as socketpair returned successfully
PreparedArg::Rpc { socket: void }
}
Arg::BinaryName => PreparedArg::BinaryName,
Arg::Entrypoint => PreparedArg::Entrypoint,
Arg::Trigger => PreparedArg::Trigger,
@ -191,6 +238,10 @@ impl PreparedArg {
Ok(vec![CString::new(socket.into_raw_fd().to_string()).unwrap()])
}
PreparedArg::Rpc { socket } => {
Ok(vec![CString::new(socket.into_raw_fd().to_string()).unwrap()])
}
PreparedArg::Trailing => Ok(spawner
.binary_args
.iter()

View File

@ -1,8 +1,10 @@
use log::{debug, error, info};
mod args;
mod rpc;
use args::PreparedArgs;
use rpc::RpcHandler;
use crate::specification::{Arg, Entrypoint, Environment, Specification, Trigger};
use crate::void::VoidBuilder;

300
src/spawner/rpc.rs Normal file
View File

@ -0,0 +1,300 @@
use log::{debug, error};
use crate::specification::{AddressFamily as SpecAddressFamily, RpcSpecification};
use crate::Error;
use std::ffi::CStr;
use std::fs::File;
use std::net::{TcpStream, UdpSocket};
use std::os::raw::c_char;
use std::os::unix::io::AsRawFd;
use nix::sys::socket::AddressFamily;
use nix::sys::socket::{recv, send, sendmsg, ControlMessage, MsgFlags};
const MAX_MSG_LENGTH: usize = 4096;
pub struct RpcHandler<'a> {
permitted_rpcs: &'a [RpcSpecification],
}
impl<'a> RpcHandler<'a> {
pub(super) fn new(permitted_rpcs: &'a [RpcSpecification]) -> Self {
Self { permitted_rpcs }
}
pub(super) fn handle(&self, socket: File) -> Result<(), Error> {
let mut buf = vec![0; MAX_MSG_LENGTH];
loop {
let read_bytes =
recv(socket.as_raw_fd(), &mut buf, MsgFlags::empty()).map_err(|e| Error::Nix {
msg: "recvmsg",
src: e,
})?;
debug!("handling rpc");
if read_bytes < 4 {
error!("received rpc too short");
continue;
}
// SAFETY: safe as the enum repr is non_exhaustive so any value is valid and the buffer is long enough
let kind = unsafe { *(buf.as_ptr() as *const RpcKind) };
let fds = Vec::new();
if kind.num_fds() > 0 {
// get any fds to go alongside the message
// nothing which requires this currently exists
unimplemented!()
}
let resp = handle_rpc(self.permitted_rpcs, kind, &buf[4..], &fds);
let (msg, fds) = RpcResultSend::new(resp);
// sendmsg first so its there when listening for the send
if !fds.is_empty() {
let fds: Box<[i32]> = fds.iter().map(|f| f.as_raw_fd()).collect();
sendmsg::<()>(
socket.as_raw_fd(),
&[],
&[ControlMessage::ScmRights(&fds)],
MsgFlags::empty(),
None,
)
.map_err(|e| Error::Nix {
msg: "sendmsg",
src: e,
})?;
}
// SAFETY: safe as msg is of fixed size
let msg = unsafe {
std::slice::from_raw_parts(
&msg as *const RpcResultSend as *const u8,
std::mem::size_of_val(&msg),
)
};
send(socket.as_raw_fd(), msg, MsgFlags::empty()).map_err(|e| Error::Nix {
msg: "send",
src: e,
})?;
}
}
}
#[repr(u32)]
#[non_exhaustive]
#[allow(dead_code)]
#[derive(Clone, Copy)]
pub enum RpcKind {
OpenTcpSocket,
OpenUdpSocket,
}
impl RpcKind {
fn num_fds(&self) -> usize {
match self {
RpcKind::OpenTcpSocket => 0,
RpcKind::OpenUdpSocket => 0,
}
}
}
pub struct OpenSocket {
pub family: AddressFamily,
pub port: u16,
pub host: [c_char],
}
pub enum RpcResult {
OpenTcpSocket { socket: TcpStream },
OpenUdpSocket { socket: UdpSocket },
Error { error: RpcError },
}
pub enum RpcResultSend {
OpenTcpSocket,
OpenUdpSocket,
Error { error: RpcError },
}
impl RpcResultSend {
fn new(from: RpcResult) -> (Self, Vec<Box<dyn AsRawFd>>) {
match from {
RpcResult::OpenTcpSocket { socket } => (Self::OpenTcpSocket, vec![Box::new(socket)]),
RpcResult::OpenUdpSocket { socket } => (Self::OpenUdpSocket, vec![Box::new(socket)]),
RpcResult::Error { error } => (Self::Error { error }, vec![]),
}
}
}
#[repr(C)]
pub enum RpcError {
BadlyFormedRequest,
OperationNotPermitted,
Io { errno: i32 },
}
fn handle_rpc(
permitted_rpcs: &[RpcSpecification],
kind: RpcKind,
data: &[u8],
_fds: &[File],
) -> RpcResult {
fn inner(
permitted_rpcs: &[RpcSpecification],
kind: RpcKind,
data: &[u8],
) -> Result<RpcResult, RpcError> {
match kind {
RpcKind::OpenTcpSocket => {
let data = unsafe { &*(data as *const [u8] as *const OpenSocket) };
if !validate_open_tcp_socket(permitted_rpcs, data)? {
Ok(RpcResult::Error {
error: RpcError::OperationNotPermitted,
})
} else {
handle_open_tcp_socket(data)
}
}
RpcKind::OpenUdpSocket => {
let data = unsafe { &*(data as *const [u8] as *const OpenSocket) };
if !validate_open_udp_socket(permitted_rpcs, data)? {
Ok(RpcResult::Error {
error: RpcError::OperationNotPermitted,
})
} else {
handle_open_udp_socket(data)
}
}
}
}
match inner(permitted_rpcs, kind, data) {
Ok(o) => o,
Err(e) => RpcResult::Error { error: e },
}
}
fn validate_open_tcp_socket(
permitted_rpcs: &[RpcSpecification],
req: &OpenSocket,
) -> Result<bool, RpcError> {
for each in permitted_rpcs {
if let RpcSpecification::OpenTcpSocket { family, port, host } = each {
let mut allowed = true;
allowed &= match family {
None => true,
Some(fam) => match req.family {
AddressFamily::Inet => *fam == SpecAddressFamily::Inet,
AddressFamily::Inet6 => *fam == SpecAddressFamily::Inet6,
_ => false,
},
};
allowed &= match port {
None => true,
Some(p) => req.port == *p,
};
allowed &= match host {
None => true,
Some(h) => {
CStr::from_bytes_with_nul(as_u8_slice(&req.host))
.map_err(|_| RpcError::BadlyFormedRequest)?
.to_string_lossy()
.as_ref()
== h
}
};
if allowed {
return Ok(true);
}
}
}
Ok(false)
}
fn handle_open_tcp_socket(req: &OpenSocket) -> Result<RpcResult, RpcError> {
let host = CStr::from_bytes_with_nul(as_u8_slice(&req.host))
.map_err(|_| RpcError::BadlyFormedRequest)?;
let host = host.to_str().map_err(|_| RpcError::BadlyFormedRequest)?;
let socket = TcpStream::connect(host).map_err(|e| RpcError::Io {
errno: e.raw_os_error().unwrap(),
})?;
Ok(RpcResult::OpenTcpSocket { socket })
}
fn validate_open_udp_socket(
permitted_rpcs: &[RpcSpecification],
req: &OpenSocket,
) -> Result<bool, RpcError> {
for each in permitted_rpcs {
if let RpcSpecification::OpenUdpSocket { family, port, host } = each {
let mut allowed = true;
allowed &= match family {
None => true,
Some(fam) => match req.family {
AddressFamily::Inet => *fam == SpecAddressFamily::Inet,
AddressFamily::Inet6 => *fam == SpecAddressFamily::Inet6,
_ => false,
},
};
allowed &= match port {
None => true,
Some(p) => req.port == *p,
};
allowed &= match host {
None => true,
Some(h) => {
CStr::from_bytes_with_nul(as_u8_slice(&req.host))
.map_err(|_| RpcError::BadlyFormedRequest)?
.to_string_lossy()
.as_ref()
== h
}
};
if allowed {
return Ok(true);
}
}
}
Ok(false)
}
fn handle_open_udp_socket(req: &OpenSocket) -> Result<RpcResult, RpcError> {
let host = CStr::from_bytes_with_nul(as_u8_slice(&req.host))
.map_err(|_| RpcError::BadlyFormedRequest)?;
let host = host.to_str().map_err(|_| RpcError::BadlyFormedRequest)?;
let socket = UdpSocket::bind("0.0.0.0:0").map_err(|e| RpcError::Io {
errno: e.raw_os_error().unwrap(),
})?;
socket.connect(host).map_err(|e| RpcError::Io {
errno: e.raw_os_error().unwrap(),
})?;
Ok(RpcResult::OpenUdpSocket { socket })
}
fn as_u8_slice(s: &[c_char]) -> &[u8] {
unsafe { std::slice::from_raw_parts(s.as_ptr() as *const u8, s.len()) }
}

View File

@ -68,6 +68,9 @@ pub enum Arg {
/// A TCP Listener
TcpListener { addr: SocketAddr },
/// An RPC socket that accepts specified commands
Rpc(Vec<RpcSpecification>),
/// The rest of argv[1..], 0 or more arguments
Trailing,
}
@ -78,6 +81,38 @@ impl Arg {
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub enum RpcSpecification {
/// Open a TCP socket
///
/// None for each value means that any value is allowed in the call.
/// A specified value restricts to exactly that.
OpenTcpSocket {
family: Option<AddressFamily>,
port: Option<u16>,
host: Option<String>,
},
/// Open a UDP socket
///
/// None for each value means that any value is allowed in the call.
/// A specified value restricts to exactly that.
OpenUdpSocket {
family: Option<AddressFamily>,
port: Option<u16>,
host: Option<String>,
},
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub enum AddressFamily {
/// IPv4 address
Inet,
/// IPv6 address
Inet6,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub enum Pipe {
Rx(String),