Merge Rosenpass API infrastructure

Pull request #388 from rosenpass/dev/karo/api
This commit is contained in:
Karolin Varner
2024-08-08 22:02:04 +02:00
committed by GitHub
68 changed files with 2879 additions and 138 deletions
+4 -1
View File
@@ -21,4 +21,7 @@ _markdown_*
**/result-*
.direnv
/output
# Visual studio code
.vscode
/output
Generated
+14
View File
@@ -1074,6 +1074,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "home"
version = "0.5.9"
@@ -1930,6 +1936,9 @@ dependencies = [
"criterion",
"derive_builder 0.20.0",
"env_logger",
"heck",
"hex",
"hex-literal",
"home",
"log",
"memoffset 0.9.1",
@@ -1948,10 +1957,12 @@ dependencies = [
"serial_test",
"stacker",
"static_assertions",
"tempfile",
"test_bin",
"thiserror",
"toml",
"zerocopy",
"zeroize",
]
[[package]]
@@ -2039,9 +2050,12 @@ version = "0.1.0"
dependencies = [
"anyhow",
"base64ct",
"mio 1.0.1",
"rustix",
"static_assertions",
"thiserror",
"typenum",
"zerocopy",
"zeroize",
]
+8 -5
View File
@@ -51,13 +51,13 @@ arbitrary = { version = "1.3.2", features = ["derive"] }
anyhow = { version = "1.0.86", features = ["backtrace", "std"] }
mio = { version = "1.0.1", features = ["net", "os-poll"] }
oqs-sys = { version = "0.9.1", default-features = false, features = [
'classic_mceliece',
'kyber',
'classic_mceliece',
'kyber',
] }
blake2 = "0.10.6"
chacha20poly1305 = { version = "0.10.1", default-features = false, features = [
"std",
"heapless",
"std",
"heapless",
] }
zerocopy = { version = "0.7.35", features = ["derive"] }
home = "0.5.9"
@@ -65,6 +65,9 @@ derive_builder = "0.20.0"
tokio = { version = "1.39", features = ["macros", "rt-multi-thread"] }
postcard= {version = "1.0.8", features = ["alloc"]}
libcrux = { version = "0.0.2-pre.2" }
hex-literal = { version = "0.4.1" }
hex = { version = "0.4.3" }
heck = { version = "0.5.0" }
#Dev dependencies
serial_test = "3.1.1"
@@ -80,4 +83,4 @@ procspawn = {version = "1.0.0", features= ["test-support"]}
#Broker dependencies (might need cleanup or changes)
wireguard-uapi = { version = "3.0.0", features = ["xplatform"] }
command-fds = "0.2.3"
rustix = { version = "0.38.27", features = ["net"] }
rustix = { version = "0.38.27", features = ["net", "fs"] }
-2
View File
@@ -10,8 +10,6 @@
//! The [KEM] Trait describes the basic API offered by a Key Encapsulation
//! Mechanism. Two implementations for it are provided, [StaticKEM] and [EphemeralKEM].
use std::result::Result;
/// Key Encapsulation Mechanism
///
/// The KEM interface defines three operations: Key generation, key encapsulation and key
+5 -3
View File
@@ -9,10 +9,12 @@ const_assert!(KEY_LEN == hash_domain::KEY_LEN);
/// Authenticated encryption with associated data
pub mod aead {
#[cfg(not(feature = "libcrux"))]
pub use crate::subtle::chacha20poly1305_ietf::{decrypt, encrypt, KEY_LEN, NONCE_LEN, TAG_LEN};
#[cfg(feature = "libcrux")]
#[cfg(not(feature = "experiment_libcrux"))]
pub use crate::subtle::chacha20poly1305_ietf::{decrypt, encrypt, KEY_LEN, NONCE_LEN, TAG_LEN};
#[cfg(feature = "experiment_libcrux")]
pub use crate::subtle::chacha20poly1305_ietf_libcrux::{
decrypt, encrypt, KEY_LEN, NONCE_LEN, TAG_LEN,
};
}
/// Authenticated encryption with associated data with a constant nonce
+2 -2
View File
@@ -1,7 +1,7 @@
pub mod blake2b;
#[cfg(not(feature = "libcrux"))]
#[cfg(not(feature = "experiment_libcrux"))]
pub mod chacha20poly1305_ietf;
#[cfg(feature = "libcrux")]
#[cfg(feature = "experiment_libcrux")]
pub mod chacha20poly1305_ietf_libcrux;
pub mod incorrect_hmac_blake2b;
pub mod xchacha20poly1305_ietf;
+5 -7
View File
@@ -7,18 +7,16 @@
///
/// The execution time of the function grows approx. linear with the length of the input. This is
/// considered safe.
///
/// ## Tests
/// [`tests::memcmp_runs_in_constant_time`] runs a stasticial test that the equality of the two
/// input parameters does not correlate with the run time.
///
/// For discussion on how to (further) ensure the constant-time execution of this function,
/// see <https://github.com/rosenpass/rosenpass/issues/232>
#[inline]
pub fn memcmp(a: &[u8], b: &[u8]) -> bool {
a.len() == b.len() && unsafe { memsec::memeq(a.as_ptr(), b.as_ptr(), a.len()) }
}
/// [tests::memcmp_runs_in_constant_time] runs a stasticial test that the equality of the two
/// input parameters does not correlate with the run time.
///
/// For discussion on how to (further) ensure the constant-time execution of this function,
/// see <https://github.com/rosenpass/rosenpass/issues/232>
#[cfg(all(test, feature = "constant_time_tests"))]
mod tests {
use super::*;
+1 -1
View File
@@ -411,12 +411,12 @@
inherit (packages.proof-proverif) CRYPTOVERIF_LIB;
inputsFrom = [ packages.default ];
nativeBuildInputs = with pkgs; [
inputs.fenix.packages.${system}.complete.toolchain
cmake # override the fakecmake from the main step above
cargo-release
clippy
nodePackages.prettier
nushell # for the .ci/gen-workflow-files.nu script
rustfmt
packages.proverif-patched
];
};
+1 -2
View File
@@ -15,8 +15,7 @@ pub struct Input {
}
fuzz_target!(|input: Input| {
let mut ciphertext: Vec<u8> = Vec::with_capacity(input.plaintext.len() + 16);
ciphertext.resize(input.plaintext.len() + 16, 0);
let mut ciphertext = vec![0u8; input.plaintext.len() + 16];
aead::encrypt(
ciphertext.as_mut_slice(),
+18 -1
View File
@@ -13,6 +13,15 @@ readme = "readme.md"
name = "rosenpass"
path = "src/main.rs"
[[bin]]
name = "rosenpass-gen-ipc-msg-types"
path = "src/bin/gen-ipc-msg-types.rs"
required-features = ["experiment_api", "internal_bin_gen_ipc_msg_types"]
[[test]]
name = "api-integration-tests"
required-features = ["experiment_api", "internal_testing"]
[[bench]]
name = "handshake"
harness = false
@@ -40,6 +49,10 @@ zerocopy = { workspace = true }
home = { workspace = true }
derive_builder = {workspace = true}
rosenpass-wireguard-broker = {workspace = true}
zeroize = { workspace = true }
hex-literal = { workspace = true, optional = true }
hex = { workspace = true, optional = true }
heck = { workspace = true, optional = true }
[build-dependencies]
anyhow = { workspace = true }
@@ -50,8 +63,12 @@ test_bin = { workspace = true }
stacker = { workspace = true }
serial_test = {workspace = true}
procspawn = {workspace = true}
tempfile = { workspace = true }
[features]
enable_broker_api = ["rosenpass-wireguard-broker/enable_broker_api"]
enable_memfd_alloc = []
experiment_memfd_secret = []
experiment_libcrux = ["rosenpass-ciphers/experiment_libcrux"]
experiment_api = ["hex-literal"]
internal_testing = []
internal_bin_gen_ipc_msg_types = ["hex", "heck"]
@@ -0,0 +1,116 @@
use zerocopy::{ByteSlice, Ref};
use rosenpass_util::zerocopy::{RefMaker, ZerocopySliceExt};
use super::{
PingRequest, PingResponse, RawMsgType, RefMakerRawMsgTypeExt, RequestMsgType, RequestRef,
ResponseMsgType, ResponseRef,
};
pub trait ByteSliceRefExt: ByteSlice {
fn msg_type_maker(self) -> RefMaker<Self, RawMsgType> {
self.zk_ref_maker()
}
fn msg_type(self) -> anyhow::Result<Ref<Self, PingRequest>> {
self.zk_parse()
}
fn msg_type_from_prefix(self) -> anyhow::Result<Ref<Self, PingRequest>> {
self.zk_parse_prefix()
}
fn msg_type_from_suffix(self) -> anyhow::Result<Ref<Self, PingRequest>> {
self.zk_parse_suffix()
}
fn request_msg_type(self) -> anyhow::Result<RequestMsgType> {
self.msg_type_maker().parse_request_msg_type()
}
fn request_msg_type_from_prefix(self) -> anyhow::Result<RequestMsgType> {
self.msg_type_maker()
.from_prefix()?
.parse_request_msg_type()
}
fn request_msg_type_from_suffix(self) -> anyhow::Result<RequestMsgType> {
self.msg_type_maker()
.from_suffix()?
.parse_request_msg_type()
}
fn response_msg_type(self) -> anyhow::Result<ResponseMsgType> {
self.msg_type_maker().parse_response_msg_type()
}
fn response_msg_type_from_prefix(self) -> anyhow::Result<ResponseMsgType> {
self.msg_type_maker()
.from_prefix()?
.parse_response_msg_type()
}
fn response_msg_type_from_suffix(self) -> anyhow::Result<ResponseMsgType> {
self.msg_type_maker()
.from_suffix()?
.parse_response_msg_type()
}
fn parse_request(self) -> anyhow::Result<RequestRef<Self>> {
RequestRef::parse(self)
}
fn parse_request_from_prefix(self) -> anyhow::Result<RequestRef<Self>> {
RequestRef::parse_from_prefix(self)
}
fn parse_request_from_suffix(self) -> anyhow::Result<RequestRef<Self>> {
RequestRef::parse_from_suffix(self)
}
fn parse_response(self) -> anyhow::Result<ResponseRef<Self>> {
ResponseRef::parse(self)
}
fn parse_response_from_prefix(self) -> anyhow::Result<ResponseRef<Self>> {
ResponseRef::parse_from_prefix(self)
}
fn parse_response_from_suffix(self) -> anyhow::Result<ResponseRef<Self>> {
ResponseRef::parse_from_suffix(self)
}
fn ping_request_maker(self) -> RefMaker<Self, PingRequest> {
self.zk_ref_maker()
}
fn ping_request(self) -> anyhow::Result<Ref<Self, PingRequest>> {
self.zk_parse()
}
fn ping_request_from_prefix(self) -> anyhow::Result<Ref<Self, PingRequest>> {
self.zk_parse_prefix()
}
fn ping_request_from_suffix(self) -> anyhow::Result<Ref<Self, PingRequest>> {
self.zk_parse_suffix()
}
fn ping_response_maker(self) -> RefMaker<Self, PingResponse> {
self.zk_ref_maker()
}
fn ping_response(self) -> anyhow::Result<Ref<Self, PingResponse>> {
self.zk_parse()
}
fn ping_response_from_prefix(self) -> anyhow::Result<Ref<Self, PingResponse>> {
self.zk_parse_prefix()
}
fn ping_response_from_suffix(self) -> anyhow::Result<Ref<Self, PingResponse>> {
self.zk_parse_suffix()
}
}
impl<B: ByteSlice> ByteSliceRefExt for B {}
@@ -0,0 +1,29 @@
use zerocopy::{ByteSliceMut, Ref};
use rosenpass_util::zerocopy::RefMaker;
use super::RawMsgType;
pub trait Message {
type Payload;
type MessageClass: Into<RawMsgType>;
const MESSAGE_TYPE: Self::MessageClass;
fn from_payload(payload: Self::Payload) -> Self;
fn init(&mut self);
fn setup<B: ByteSliceMut>(buf: B) -> anyhow::Result<Ref<B, Self>>;
}
pub trait ZerocopyResponseMakerSetupMessageExt<B, T> {
fn setup_msg(self) -> anyhow::Result<Ref<B, T>>;
}
impl<B, T> ZerocopyResponseMakerSetupMessageExt<B, T> for RefMaker<B, T>
where
B: ByteSliceMut,
T: Message,
{
fn setup_msg(self) -> anyhow::Result<Ref<B, T>> {
T::setup(self.into_buf())
}
}
@@ -0,0 +1,117 @@
use hex_literal::hex;
use rosenpass_util::zerocopy::RefMaker;
use zerocopy::ByteSlice;
use crate::RosenpassError::{self, InvalidApiMessageType};
pub type RawMsgType = u128;
// constants generated by gen-ipc-msg-types:
// hash domain hash of: Rosenpass IPC API -> Rosenpass Protocol Server -> Ping Request
pub const PING_REQUEST: RawMsgType =
RawMsgType::from_le_bytes(hex!("2397 3ecc c441 704d 0b02 ea31 45d3 4999"));
// hash domain hash of: Rosenpass IPC API -> Rosenpass Protocol Server -> Ping Response
pub const PING_RESPONSE: RawMsgType =
RawMsgType::from_le_bytes(hex!("4ec7 f6f0 2bbc ba64 48f1 da14 c7cf 0260"));
pub trait MessageAttributes {
fn message_size(&self) -> usize;
}
#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
pub enum RequestMsgType {
Ping,
}
#[derive(Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
pub enum ResponseMsgType {
Ping,
}
impl MessageAttributes for RequestMsgType {
fn message_size(&self) -> usize {
match self {
Self::Ping => std::mem::size_of::<super::PingRequest>(),
}
}
}
impl MessageAttributes for ResponseMsgType {
fn message_size(&self) -> usize {
match self {
Self::Ping => std::mem::size_of::<super::PingResponse>(),
}
}
}
impl TryFrom<RawMsgType> for RequestMsgType {
type Error = RosenpassError;
fn try_from(value: RawMsgType) -> Result<Self, Self::Error> {
use RequestMsgType as E;
Ok(match value {
self::PING_REQUEST => E::Ping,
_ => return Err(InvalidApiMessageType(value)),
})
}
}
impl From<RequestMsgType> for RawMsgType {
fn from(val: RequestMsgType) -> Self {
use RequestMsgType as E;
match val {
E::Ping => self::PING_REQUEST,
}
}
}
impl TryFrom<RawMsgType> for ResponseMsgType {
type Error = RosenpassError;
fn try_from(value: RawMsgType) -> Result<Self, Self::Error> {
use ResponseMsgType as E;
Ok(match value {
self::PING_RESPONSE => E::Ping,
_ => return Err(InvalidApiMessageType(value)),
})
}
}
impl From<ResponseMsgType> for RawMsgType {
fn from(val: ResponseMsgType) -> Self {
use ResponseMsgType as E;
match val {
E::Ping => self::PING_RESPONSE,
}
}
}
pub trait RawMsgTypeExt {
fn into_request_msg_type(self) -> Result<RequestMsgType, RosenpassError>;
fn into_response_msg_type(self) -> Result<ResponseMsgType, RosenpassError>;
}
impl RawMsgTypeExt for RawMsgType {
fn into_request_msg_type(self) -> Result<RequestMsgType, RosenpassError> {
self.try_into()
}
fn into_response_msg_type(self) -> Result<ResponseMsgType, RosenpassError> {
self.try_into()
}
}
pub trait RefMakerRawMsgTypeExt {
fn parse_request_msg_type(self) -> anyhow::Result<RequestMsgType>;
fn parse_response_msg_type(self) -> anyhow::Result<ResponseMsgType>;
}
impl<B: ByteSlice> RefMakerRawMsgTypeExt for RefMaker<B, RawMsgType> {
fn parse_request_msg_type(self) -> anyhow::Result<RequestMsgType> {
Ok(self.parse()?.read().try_into()?)
}
fn parse_response_msg_type(self) -> anyhow::Result<ResponseMsgType> {
Ok(self.parse()?.read().try_into()?)
}
}
+17
View File
@@ -0,0 +1,17 @@
mod byte_slice_ext;
mod message_trait;
mod message_type;
mod payload;
mod request_ref;
mod request_response;
mod response_ref;
mod server;
pub use byte_slice_ext::*;
pub use message_trait::*;
pub use message_type::*;
pub use payload::*;
pub use request_ref::*;
pub use request_response::*;
pub use response_ref::*;
pub use server::*;
+96
View File
@@ -0,0 +1,96 @@
use rosenpass_util::zerocopy::ZerocopyMutSliceExt;
use zerocopy::{AsBytes, ByteSliceMut, FromBytes, FromZeroes, Ref};
use super::{Message, RawMsgType, RequestMsgType, ResponseMsgType};
/// Size required to fit any message in binary form
pub const MAX_REQUEST_LEN: usize = 2500; // TODO fix this
pub const MAX_RESPONSE_LEN: usize = 2500; // TODO fix this
#[repr(packed)]
#[derive(Debug, Copy, Clone, Hash, AsBytes, FromBytes, FromZeroes, PartialEq, Eq)]
pub struct Envelope<M: AsBytes + FromBytes> {
/// Which message this is
pub msg_type: RawMsgType,
/// The actual Paylod
pub payload: M,
}
pub type RequestEnvelope<M> = Envelope<M>;
pub type ResponseEnvelope<M> = Envelope<M>;
#[repr(packed)]
#[derive(Debug, Copy, Clone, Hash, AsBytes, FromBytes, FromZeroes, PartialEq, Eq)]
pub struct PingRequestPayload {
/// Randomly generated connection id
pub echo: [u8; 256],
}
pub type PingRequest = RequestEnvelope<PingRequestPayload>;
impl PingRequest {
pub fn new(echo: [u8; 256]) -> Self {
Self::from_payload(PingRequestPayload { echo })
}
}
impl Message for PingRequest {
type Payload = PingRequestPayload;
type MessageClass = RequestMsgType;
const MESSAGE_TYPE: Self::MessageClass = RequestMsgType::Ping;
fn from_payload(payload: Self::Payload) -> Self {
Self {
msg_type: Self::MESSAGE_TYPE.into(),
payload,
}
}
fn setup<B: ByteSliceMut>(buf: B) -> anyhow::Result<Ref<B, Self>> {
let mut r: Ref<B, Self> = buf.zk_zeroized()?;
r.init();
Ok(r)
}
fn init(&mut self) {
self.msg_type = Self::MESSAGE_TYPE.into();
}
}
#[repr(packed)]
#[derive(Debug, Copy, Clone, Hash, AsBytes, FromBytes, FromZeroes, PartialEq, Eq)]
pub struct PingResponsePayload {
/// Randomly generated connection id
pub echo: [u8; 256],
}
pub type PingResponse = ResponseEnvelope<PingResponsePayload>;
impl PingResponse {
pub fn new(echo: [u8; 256]) -> Self {
Self::from_payload(PingResponsePayload { echo })
}
}
impl Message for PingResponse {
type Payload = PingResponsePayload;
type MessageClass = ResponseMsgType;
const MESSAGE_TYPE: Self::MessageClass = ResponseMsgType::Ping;
fn from_payload(payload: Self::Payload) -> Self {
Self {
msg_type: Self::MESSAGE_TYPE.into(),
payload,
}
}
fn setup<B: ByteSliceMut>(buf: B) -> anyhow::Result<Ref<B, Self>> {
let mut r: Ref<B, Self> = buf.zk_zeroized()?;
r.init();
Ok(r)
}
fn init(&mut self) {
self.msg_type = Self::MESSAGE_TYPE.into();
}
}
@@ -0,0 +1,107 @@
use anyhow::ensure;
use zerocopy::{ByteSlice, ByteSliceMut, Ref};
use super::{ByteSliceRefExt, MessageAttributes, PingRequest, RequestMsgType};
struct RequestRefMaker<B> {
buf: B,
msg_type: RequestMsgType,
}
impl<B: ByteSlice> RequestRef<B> {
pub fn parse(buf: B) -> anyhow::Result<Self> {
RequestRefMaker::new(buf)?.parse()
}
pub fn parse_from_prefix(buf: B) -> anyhow::Result<Self> {
RequestRefMaker::new(buf)?.from_prefix()?.parse()
}
pub fn parse_from_suffix(buf: B) -> anyhow::Result<Self> {
RequestRefMaker::new(buf)?.from_suffix()?.parse()
}
pub fn message_type(&self) -> RequestMsgType {
match self {
Self::Ping(_) => RequestMsgType::Ping,
}
}
}
impl<B> From<Ref<B, PingRequest>> for RequestRef<B> {
fn from(v: Ref<B, PingRequest>) -> Self {
Self::Ping(v)
}
}
impl<B: ByteSlice> RequestRefMaker<B> {
fn new(buf: B) -> anyhow::Result<Self> {
let msg_type = buf.deref().request_msg_type_from_prefix()?;
Ok(Self { buf, msg_type })
}
fn target_size(&self) -> usize {
self.msg_type.message_size()
}
fn parse(self) -> anyhow::Result<RequestRef<B>> {
Ok(match self.msg_type {
RequestMsgType::Ping => RequestRef::Ping(self.buf.ping_request()?),
})
}
#[allow(clippy::wrong_self_convention)]
fn from_prefix(self) -> anyhow::Result<Self> {
self.ensure_fit()?;
let point = self.target_size();
let Self { buf, msg_type } = self;
let (buf, _) = buf.split_at(point);
Ok(Self { buf, msg_type })
}
#[allow(clippy::wrong_self_convention)]
fn from_suffix(self) -> anyhow::Result<Self> {
self.ensure_fit()?;
let point = self.buf.len() - self.target_size();
let Self { buf, msg_type } = self;
let (buf, _) = buf.split_at(point);
Ok(Self { buf, msg_type })
}
pub fn ensure_fit(&self) -> anyhow::Result<()> {
let have = self.buf.len();
let need = self.target_size();
ensure!(
need <= have,
"Buffer is undersized at {have} bytes (need {need} bytes)!"
);
Ok(())
}
}
pub enum RequestRef<B> {
Ping(Ref<B, PingRequest>),
}
impl<B> RequestRef<B>
where
B: ByteSlice,
{
pub fn bytes(&self) -> &[u8] {
match self {
Self::Ping(r) => r.bytes(),
}
}
}
impl<B> RequestRef<B>
where
B: ByteSliceMut,
{
pub fn bytes_mut(&mut self) -> &[u8] {
match self {
Self::Ping(r) => r.bytes_mut(),
}
}
}
@@ -0,0 +1,103 @@
use rosenpass_util::zerocopy::{
RefMaker, ZerocopyEmancipateExt, ZerocopyEmancipateMutExt, ZerocopySliceExt,
};
use zerocopy::{ByteSlice, ByteSliceMut, Ref};
use super::{Message, PingRequest, PingResponse};
use super::{RequestRef, ResponseRef, ZerocopyResponseMakerSetupMessageExt};
pub trait RequestMsg: Sized + Message {
type ResponseMsg: ResponseMsg;
fn zk_response_maker<B: ByteSlice>(buf: B) -> RefMaker<B, Self::ResponseMsg> {
buf.zk_ref_maker()
}
fn setup_response<B: ByteSliceMut>(buf: B) -> anyhow::Result<Ref<B, Self::ResponseMsg>> {
Self::zk_response_maker(buf).setup_msg()
}
fn setup_response_from_prefix<B: ByteSliceMut>(
buf: B,
) -> anyhow::Result<Ref<B, Self::ResponseMsg>> {
Self::zk_response_maker(buf).from_prefix()?.setup_msg()
}
fn setup_response_from_suffix<B: ByteSliceMut>(
buf: B,
) -> anyhow::Result<Ref<B, Self::ResponseMsg>> {
Self::zk_response_maker(buf).from_prefix()?.setup_msg()
}
}
pub trait ResponseMsg: Message {
type RequestMsg: RequestMsg;
}
impl RequestMsg for PingRequest {
type ResponseMsg = PingResponse;
}
impl ResponseMsg for PingResponse {
type RequestMsg = PingRequest;
}
pub type PingPair<B1, B2> = (Ref<B1, PingRequest>, Ref<B2, PingResponse>);
pub enum RequestResponsePair<B1, B2> {
Ping(PingPair<B1, B2>),
}
impl<B1, B2> From<PingPair<B1, B2>> for RequestResponsePair<B1, B2> {
fn from(v: PingPair<B1, B2>) -> Self {
RequestResponsePair::Ping(v)
}
}
impl<B1, B2> RequestResponsePair<B1, B2>
where
B1: ByteSlice,
B2: ByteSlice,
{
pub fn both(&self) -> (RequestRef<&[u8]>, ResponseRef<&[u8]>) {
match self {
Self::Ping((req, res)) => {
let req = RequestRef::Ping(req.emancipate());
let res = ResponseRef::Ping(res.emancipate());
(req, res)
}
}
}
pub fn request(&self) -> RequestRef<&[u8]> {
self.both().0
}
pub fn response(&self) -> ResponseRef<&[u8]> {
self.both().1
}
}
impl<B1, B2> RequestResponsePair<B1, B2>
where
B1: ByteSliceMut,
B2: ByteSliceMut,
{
pub fn both_mut(&mut self) -> (RequestRef<&mut [u8]>, ResponseRef<&mut [u8]>) {
match self {
Self::Ping((req, res)) => {
let req = RequestRef::Ping(req.emancipate_mut());
let res = ResponseRef::Ping(res.emancipate_mut());
(req, res)
}
}
}
pub fn request_mut(&mut self) -> RequestRef<&mut [u8]> {
self.both_mut().0
}
pub fn response_mut(&mut self) -> ResponseRef<&mut [u8]> {
self.both_mut().1
}
}
@@ -0,0 +1,108 @@
// TODO: This is copied verbatim from ResponseRef…not pretty
use anyhow::ensure;
use zerocopy::{ByteSlice, ByteSliceMut, Ref};
use super::{ByteSliceRefExt, MessageAttributes, PingResponse, ResponseMsgType};
struct ResponseRefMaker<B> {
buf: B,
msg_type: ResponseMsgType,
}
impl<B: ByteSlice> ResponseRef<B> {
pub fn parse(buf: B) -> anyhow::Result<Self> {
ResponseRefMaker::new(buf)?.parse()
}
pub fn parse_from_prefix(buf: B) -> anyhow::Result<Self> {
ResponseRefMaker::new(buf)?.from_prefix()?.parse()
}
pub fn parse_from_suffix(buf: B) -> anyhow::Result<Self> {
ResponseRefMaker::new(buf)?.from_suffix()?.parse()
}
pub fn message_type(&self) -> ResponseMsgType {
match self {
Self::Ping(_) => ResponseMsgType::Ping,
}
}
}
impl<B> From<Ref<B, PingResponse>> for ResponseRef<B> {
fn from(v: Ref<B, PingResponse>) -> Self {
Self::Ping(v)
}
}
impl<B: ByteSlice> ResponseRefMaker<B> {
fn new(buf: B) -> anyhow::Result<Self> {
let msg_type = buf.deref().response_msg_type_from_prefix()?;
Ok(Self { buf, msg_type })
}
fn target_size(&self) -> usize {
self.msg_type.message_size()
}
fn parse(self) -> anyhow::Result<ResponseRef<B>> {
Ok(match self.msg_type {
ResponseMsgType::Ping => ResponseRef::Ping(self.buf.ping_response()?),
})
}
#[allow(clippy::wrong_self_convention)]
fn from_prefix(self) -> anyhow::Result<Self> {
self.ensure_fit()?;
let point = self.target_size();
let Self { buf, msg_type } = self;
let (buf, _) = buf.split_at(point);
Ok(Self { buf, msg_type })
}
#[allow(clippy::wrong_self_convention)]
fn from_suffix(self) -> anyhow::Result<Self> {
self.ensure_fit()?;
let point = self.buf.len() - self.target_size();
let Self { buf, msg_type } = self;
let (buf, _) = buf.split_at(point);
Ok(Self { buf, msg_type })
}
pub fn ensure_fit(&self) -> anyhow::Result<()> {
let have = self.buf.len();
let need = self.target_size();
ensure!(
need <= have,
"Buffer is undersized at {have} bytes (need {need} bytes)!"
);
Ok(())
}
}
pub enum ResponseRef<B> {
Ping(Ref<B, PingResponse>),
}
impl<B> ResponseRef<B>
where
B: ByteSlice,
{
pub fn bytes(&self) -> &[u8] {
match self {
Self::Ping(r) => r.bytes(),
}
}
}
impl<B> ResponseRef<B>
where
B: ByteSliceMut,
{
pub fn bytes_mut(&mut self) -> &[u8] {
match self {
Self::Ping(r) => r.bytes_mut(),
}
}
}
+40
View File
@@ -0,0 +1,40 @@
use zerocopy::{ByteSlice, ByteSliceMut};
use super::{ByteSliceRefExt, Message, PingRequest, PingResponse, RequestRef, RequestResponsePair};
pub trait Server {
fn ping(&mut self, req: &PingRequest, res: &mut PingResponse) -> anyhow::Result<()>;
fn dispatch<ReqBuf, ResBuf>(
&mut self,
p: &mut RequestResponsePair<ReqBuf, ResBuf>,
) -> anyhow::Result<()>
where
ReqBuf: ByteSlice,
ResBuf: ByteSliceMut,
{
match p {
RequestResponsePair::Ping((req, res)) => self.ping(req, res),
}
}
fn handle_message<ReqBuf, ResBuf>(&mut self, req: ReqBuf, res: ResBuf) -> anyhow::Result<usize>
where
ReqBuf: ByteSlice,
ResBuf: ByteSliceMut,
{
let req = req.parse_request_from_prefix()?;
// TODO: This is not pretty; This match should be moved into RequestRef
let mut pair = match req {
RequestRef::Ping(req) => {
let mut res = res.ping_response_from_prefix()?;
res.init();
RequestResponsePair::Ping((req, res))
}
};
self.dispatch(&mut pair)?;
let res_len = pair.request().bytes().len();
Ok(res_len)
}
}
+40
View File
@@ -0,0 +1,40 @@
use std::path::PathBuf;
use clap::Args;
use crate::config::Rosenpass as RosenpassConfig;
use super::config::ApiConfig;
#[cfg(feature = "experiment_api")]
#[derive(Args, Debug)]
pub struct ApiCli {
/// Where in the file-system to create the unix socket the rosenpass API will be listening for
/// connections on.
#[arg(long)]
api_listen_path: Vec<PathBuf>,
/// When rosenpass is called from another process, the other process can open and bind the
/// unix socket for the Rosenpass API to use themselves, passing it to this process. In Rust this can be achieved
/// using the [command-fds](https://docs.rs/command-fds/latest/command_fds/) crate.
#[arg(long)]
api_listen_fd: Vec<i32>,
/// When rosenpass is called from another process, the other process can connect the unix socket for the API
/// themselves, for instance using the `socketpair(2)` system call.
#[arg(long)]
api_stream_fd: Vec<i32>,
}
impl ApiCli {
pub fn apply_to_config(&self, cfg: &mut RosenpassConfig) -> anyhow::Result<()> {
self.apply_to_api_config(&mut cfg.api)
}
pub fn apply_to_api_config(&self, cfg: &mut ApiConfig) -> anyhow::Result<()> {
cfg.listen_path.extend_from_slice(&self.api_listen_path);
cfg.listen_fd.extend_from_slice(&self.api_listen_fd);
cfg.stream_fd.extend_from_slice(&self.api_stream_fd);
Ok(())
}
}
+41
View File
@@ -0,0 +1,41 @@
use std::path::PathBuf;
use mio::net::UnixListener;
use rosenpass_util::mio::{UnixListenerExt, UnixStreamExt};
use serde::{Deserialize, Serialize};
use crate::app_server::AppServer;
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct ApiConfig {
/// Where in the file-system to create the unix socket the rosenpass API will be listening for
/// connections on
pub listen_path: Vec<PathBuf>,
/// When rosenpass is called from another process, the other process can open and bind the
/// unix socket for the Rosenpass API to use themselves, passing it to this process. In Rust this can be achieved
/// using the [command-fds](https://docs.rs/command-fds/latest/command_fds/) crate.
pub listen_fd: Vec<i32>,
/// When rosenpass is called from another process, the other process can connect the unix socket for the API
/// themselves, for instance using the `socketpair(2)` system call.
pub stream_fd: Vec<i32>,
}
impl ApiConfig {
pub fn apply_to_app_server(&self, srv: &mut AppServer) -> anyhow::Result<()> {
for path in self.listen_path.iter() {
srv.add_api_listener(UnixListener::bind(path)?)?;
}
for fd in self.listen_fd.iter() {
srv.add_api_listener(UnixListenerExt::claim_fd(*fd)?)?;
}
for fd in self.stream_fd.iter() {
srv.add_api_connection(UnixStreamExt::claim_fd(*fd)?)?;
}
Ok(())
}
}
@@ -0,0 +1,44 @@
use rosenpass_to::{ops::copy_slice, To};
use crate::protocol::CryptoServer;
use super::Server as ApiServer;
#[derive(Debug)]
pub struct CryptoServerApiState {
_dummy: (),
}
impl CryptoServerApiState {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self { _dummy: () }
}
pub fn acquire_backend<'a>(
&'a mut self,
crypto: &'a mut Option<CryptoServer>,
) -> CryptoServerApiHandler<'a> {
let state = self;
CryptoServerApiHandler { state, crypto }
}
}
pub struct CryptoServerApiHandler<'a> {
#[allow(unused)] // TODO: Remove
crypto: &'a mut Option<CryptoServer>,
#[allow(unused)] // TODO: Remove
state: &'a mut CryptoServerApiState,
}
impl<'a> ApiServer for CryptoServerApiHandler<'a> {
fn ping(
&mut self,
req: &super::PingRequest,
res: &mut super::PingResponse,
) -> anyhow::Result<()> {
let (req, res) = (&req.payload, &mut res.payload);
copy_slice(&req.echo).to(&mut res.echo);
Ok(())
}
}
+167
View File
@@ -0,0 +1,167 @@
use mio::{net::UnixStream, Interest};
use rosenpass_util::{
io::{IoResultKindHintExt, TryIoResultKindHintExt},
length_prefix_encoding::{
decoder::{self as lpe_decoder, LengthPrefixDecoder},
encoder::{self as lpe_encoder, LengthPrefixEncoder},
},
};
use zeroize::Zeroize;
use crate::{api::Server, app_server::MioTokenDispenser, protocol::CryptoServer};
use super::super::{CryptoServerApiState, MAX_REQUEST_LEN, MAX_RESPONSE_LEN};
#[derive(Debug)]
pub struct MioConnection {
io: UnixStream,
invalid_read: bool,
read_buffer: LengthPrefixDecoder<[u8; MAX_REQUEST_LEN]>,
write_buffer: LengthPrefixEncoder<[u8; MAX_RESPONSE_LEN]>,
api_state: CryptoServerApiState,
}
impl MioConnection {
pub fn new(
mut io: UnixStream,
registry: &mio::Registry,
token_dispenser: &mut MioTokenDispenser, // TODO: We should actually start using tokens…
) -> std::io::Result<Self> {
registry.register(
&mut io,
token_dispenser.dispense(),
Interest::READABLE | Interest::WRITABLE,
)?;
let invalid_read = false;
let read_buffer = LengthPrefixDecoder::new([0u8; MAX_REQUEST_LEN]);
let write_buffer = LengthPrefixEncoder::from_buffer([0u8; MAX_RESPONSE_LEN]);
let api_state = CryptoServerApiState::new();
Ok(Self {
io,
invalid_read,
read_buffer,
write_buffer,
api_state,
})
}
pub fn poll(&mut self, crypto: &mut Option<CryptoServer>) -> anyhow::Result<()> {
self.flush_write_buffer()?;
if self.write_buffer.exhausted() {
self.recv(crypto)?;
}
Ok(())
}
// This is *exclusively* called by recv if the read_buffer holds a message
fn handle_incoming_message(&mut self, crypto: &mut Option<CryptoServer>) -> anyhow::Result<()> {
// Unwrap is allowed because recv() confirms before the call that a message was
// received
let req = self.read_buffer.message().unwrap().unwrap();
// TODO: The API should not return anyhow::Result
let response_len = self
.api_state
.acquire_backend(crypto)
.handle_message(req, self.write_buffer.buffer_bytes_mut())?;
self.read_buffer.zeroize(); // clear for new message to read
self.write_buffer
.restart_write_with_new_message(response_len)?;
self.flush_write_buffer()?;
Ok(())
}
fn flush_write_buffer(&mut self) -> anyhow::Result<()> {
if self.write_buffer.exhausted() {
return Ok(());
}
loop {
use lpe_encoder::WriteToIoReturn as Ret;
use std::io::ErrorKind as K;
match self
.write_buffer
.write_to_stdio(&self.io)
.io_err_kind_hint()
{
// Done
Ok(Ret { done: true, .. }) => {
self.write_buffer.zeroize(); // clear for new message to write
break;
}
// Would block
Ok(Ret {
bytes_written: 0, ..
}) => break,
Err((_e, K::WouldBlock)) => break,
// Just continue
Ok(_) => continue, /* Ret { bytes_written > 0, done = false } acc. to previous cases*/
Err((_e, K::Interrupted)) => continue,
// Other errors
Err((e, _ek)) => Err(e)?,
}
}
Ok(())
}
fn recv(&mut self, crypto: &mut Option<CryptoServer>) -> anyhow::Result<()> {
if !self.write_buffer.exhausted() || self.invalid_read {
return Ok(());
}
loop {
use lpe_decoder::{ReadFromIoError as E, ReadFromIoReturn as Ret};
use std::io::ErrorKind as K;
match self
.read_buffer
.read_from_stdio(&self.io)
.try_io_err_kind_hint()
{
// We actually received a proper message
// (Impl below match to appease borrow checker)
Ok(Ret {
message: Some(_msg),
..
}) => {}
// Message does not fit in buffer
Err((e @ E::MessageTooLargeError { .. }, _)) => {
log::warn!("Received message on API that was too big to fit in our buffers; \
looks like the client is broken. Stopping to process messages of the client.\n\
Error: {e:?}");
// TODO: We should properly close down the socket in this case, but to do that,
// we need to have the facilities in the Rosenpass IO handling system to close
// open connections.
// Just leaving the API connections dangling for now.
// This should be fixed for non-experimental use of the API.
self.invalid_read = true;
break;
}
// Would block
Ok(Ret { bytes_read: 0, .. }) => break,
Err((_, Some(K::WouldBlock))) => break,
// Just keep going
Ok(Ret { bytes_read: _, .. }) => continue,
Err((_, Some(K::Interrupted))) => continue,
// Other IO Error (just pass on to the caller)
Err((E::IoError(e), _)) => Err(e)?,
};
self.handle_incoming_message(crypto)?;
break; // Handle just one message, leave some room for other IO handlers
}
Ok(())
}
}
+93
View File
@@ -0,0 +1,93 @@
use std::io;
use mio::net::{UnixListener, UnixStream};
use rosenpass_util::{io::nonblocking_handle_io_errors, mio::interest::RW as MIO_RW};
use crate::{app_server::MioTokenDispenser, protocol::CryptoServer};
use super::MioConnection;
#[derive(Default, Debug)]
pub struct MioManager {
listeners: Vec<UnixListener>,
connections: Vec<MioConnection>,
}
impl MioManager {
pub fn new() -> Self {
Self::default()
}
pub fn add_listener(
&mut self,
mut listener: UnixListener,
registry: &mio::Registry,
token_dispenser: &mut MioTokenDispenser,
) -> io::Result<()> {
registry.register(&mut listener, token_dispenser.dispense(), MIO_RW)?;
self.listeners.push(listener);
Ok(())
}
pub fn add_connection(
&mut self,
connection: UnixStream,
registry: &mio::Registry,
token_dispenser: &mut MioTokenDispenser,
) -> io::Result<()> {
let connection = MioConnection::new(connection, registry, token_dispenser)?;
self.connections.push(connection);
Ok(())
}
pub fn poll(
&mut self,
crypto: &mut Option<CryptoServer>,
registry: &mio::Registry,
token_dispenser: &mut MioTokenDispenser,
) -> anyhow::Result<()> {
self.accept_connections(registry, token_dispenser)?;
self.poll_connections(crypto)?;
Ok(())
}
fn accept_connections(
&mut self,
registry: &mio::Registry,
token_dispenser: &mut MioTokenDispenser,
) -> io::Result<()> {
for idx in 0..self.listeners.len() {
self.accept_from(idx, registry, token_dispenser)?;
}
Ok(())
}
fn accept_from(
&mut self,
idx: usize,
registry: &mio::Registry,
token_dispenser: &mut MioTokenDispenser,
) -> io::Result<()> {
// Accept connection until the socket would block or returns another error
// TODO: This currently only adds connections--we eventually need the ability to remove
// them as well, see the note in connection.rs
loop {
match nonblocking_handle_io_errors(|| self.listeners[idx].accept())? {
None => break,
Some((conn, _addr)) => {
self.add_connection(conn, registry, token_dispenser)?;
}
};
}
Ok(())
}
fn poll_connections(&mut self, crypto: &mut Option<CryptoServer>) -> anyhow::Result<()> {
for conn in self.connections.iter_mut() {
conn.poll(crypto)?
}
Ok(())
}
}
+5
View File
@@ -0,0 +1,5 @@
mod connection;
mod manager;
pub use connection::*;
pub use manager::*;
+9
View File
@@ -0,0 +1,9 @@
mod boilerplate;
mod crypto_server_api_handler;
pub use boilerplate::*;
pub use crypto_server_api_handler::*;
pub mod cli;
pub mod config;
pub mod mio;
+69 -16
View File
@@ -1,5 +1,6 @@
use anyhow::bail;
use anyhow::Context;
use anyhow::Result;
use derive_builder::Builder;
use log::{error, info, warn};
@@ -16,7 +17,9 @@ use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Debug;
use std::io::stdout;
use std::io::ErrorKind;
use std::io::Write;
use std::net::Ipv4Addr;
use std::net::Ipv6Addr;
use std::net::SocketAddr;
@@ -63,7 +66,7 @@ pub struct MioTokenDispenser {
}
impl MioTokenDispenser {
fn dispense(&mut self) -> Token {
pub fn dispense(&mut self) -> Token {
let r = self.counter;
self.counter += 1;
Token(r)
@@ -144,7 +147,7 @@ pub struct AppServerTest {
// TODO add user control via unix domain socket and stdin/stdout
#[derive(Debug)]
pub struct AppServer {
pub crypt: CryptoServer,
pub crypt: Option<CryptoServer>,
pub sockets: Vec<mio::net::UdpSocket>,
pub events: mio::Events,
pub mio_poll: mio::Poll,
@@ -159,6 +162,8 @@ pub struct AppServer {
pub unpolled_count: usize,
pub last_update_time: Instant,
pub test_helpers: Option<AppServerTest>,
#[cfg(feature = "experiment_api")]
pub api_manager: crate::api::mio::MioManager,
}
/// A socket pointer is an index assigned to a socket;
@@ -601,7 +606,7 @@ impl AppServer {
// TODO use mio::net::UnixStream together with std::os::unix::net::UnixStream for Linux
Ok(Self {
crypt: CryptoServer::new(sk, pk),
crypt: Some(CryptoServer::new(sk, pk)),
peers: Vec::new(),
verbosity,
sockets,
@@ -616,9 +621,23 @@ impl AppServer {
unpolled_count: 0,
last_update_time: Instant::now(),
test_helpers,
#[cfg(feature = "experiment_api")]
api_manager: crate::api::mio::MioManager::default(),
})
}
pub fn crypto_server(&self) -> anyhow::Result<&CryptoServer> {
self.crypt
.as_ref()
.context("Cryptography handler not initialized")
}
pub fn crypto_server_mut(&mut self) -> anyhow::Result<&mut CryptoServer> {
self.crypt
.as_mut()
.context("Cryptography handler not initialized")
}
pub fn verbose(&self) -> bool {
matches!(self.verbosity, Verbosity::Verbose)
}
@@ -669,7 +688,7 @@ impl AppServer {
broker_peer: Option<BrokerPeer>,
hostname: Option<String>,
) -> anyhow::Result<AppPeerPtr> {
let PeerPtr(pn) = self.crypt.add_peer(psk, pk)?;
let PeerPtr(pn) = self.crypto_server_mut()?.add_peer(psk, pk)?;
assert!(pn == self.peers.len());
let initial_endpoint = hostname
.map(Endpoint::discovery_from_hostname)
@@ -712,7 +731,7 @@ impl AppServer {
);
if tries_left > 0 {
error!("re-initializing networking in {sleep}! {tries_left} tries left.");
std::thread::sleep(self.crypt.timebase.dur(sleep));
std::thread::sleep(self.crypto_server_mut()?.timebase.dur(sleep));
continue;
}
@@ -758,11 +777,11 @@ impl AppServer {
match self.poll(&mut *rx)? {
#[allow(clippy::redundant_closure_call)]
SendInitiation(peer) => tx_maybe_with!(peer, || self
.crypt
.crypto_server_mut()?
.initiate_handshake(peer.lower(), &mut *tx))?,
#[allow(clippy::redundant_closure_call)]
SendRetransmission(peer) => tx_maybe_with!(peer, || self
.crypt
.crypto_server_mut()?
.retransmit_handshake(peer.lower(), &mut *tx))?,
DeleteKey(peer) => {
self.output_key(peer, Stale, &SymKey::random())?;
@@ -783,7 +802,9 @@ impl AppServer {
DoSOperation::UnderLoad => {
self.handle_msg_under_load(&endpoint, &rx[..len], &mut *tx)
}
DoSOperation::Normal => self.crypt.handle_msg(&rx[..len], &mut *tx),
DoSOperation::Normal => {
self.crypto_server_mut()?.handle_msg(&rx[..len], &mut *tx)
}
};
match msg_result {
Err(ref e) => {
@@ -811,7 +832,8 @@ impl AppServer {
ap.get_app_mut(self).current_endpoint = Some(endpoint);
// TODO: Maybe we should rather call the key "rosenpass output"?
self.output_key(ap, Exchanged, &self.crypt.osk(p)?)?;
let osk = &self.crypto_server_mut()?.osk(p)?;
self.output_key(ap, Exchanged, osk)?;
}
}
}
@@ -827,9 +849,9 @@ impl AppServer {
tx: &mut [u8],
) -> Result<crate::protocol::HandleMsgResult> {
match endpoint {
Endpoint::SocketBoundAddress(socket) => {
self.crypt.handle_msg_under_load(rx, &mut *tx, socket)
}
Endpoint::SocketBoundAddress(socket) => self
.crypto_server_mut()?
.handle_msg_under_load(rx, &mut *tx, socket),
Endpoint::Discovery(_) => {
anyhow::bail!("Host-path discovery is not supported under load")
}
@@ -842,7 +864,7 @@ impl AppServer {
why: KeyOutputReason,
key: &SymKey,
) -> anyhow::Result<()> {
let peerid = peer.lower().get(&self.crypt).pidt()?;
let peerid = peer.lower().get(self.crypto_server()?).pidt()?;
if self.verbose() {
let msg = match why {
@@ -870,10 +892,14 @@ impl AppServer {
// this is intentionally writing to stdout instead of stderr, because
// it is meant to allow external detection of a successful key-exchange
println!(
let stdout = stdout();
let mut stdout = stdout.lock();
writeln!(
stdout,
"output-key peer {} key-file {of:?} {why}",
peerid.fmt_b64::<MAX_B64_PEER_ID_SIZE>()
);
)?;
stdout.flush()?;
}
peer.set_psk(self, key)?;
@@ -885,7 +911,7 @@ impl AppServer {
use crate::protocol::PollResult as C;
use AppPollResult as A;
loop {
return Ok(match self.crypt.poll()? {
return Ok(match self.crypto_server_mut()?.poll()? {
C::DeleteKey(PeerPtr(no)) => A::DeleteKey(AppPeerPtr(no)),
C::SendInitiation(PeerPtr(no)) => A::SendInitiation(AppPeerPtr(no)),
C::SendRetransmission(PeerPtr(no)) => A::SendRetransmission(AppPeerPtr(no)),
@@ -1013,6 +1039,33 @@ impl AppServer {
broker.process_poll()?;
}
// API poll
#[cfg(feature = "experiment_api")]
self.api_manager.poll(
&mut self.crypt,
self.mio_poll.registry(),
&mut self.mio_token_dispenser,
)?;
Ok(None)
}
#[cfg(feature = "experiment_api")]
pub fn add_api_connection(&mut self, connection: mio::net::UnixStream) -> std::io::Result<()> {
self.api_manager.add_connection(
connection,
self.mio_poll.registry(),
&mut self.mio_token_dispenser,
)
}
#[cfg(feature = "experiment_api")]
pub fn add_api_listener(&mut self, listener: mio::net::UnixListener) -> std::io::Result<()> {
self.api_manager.add_listener(
listener,
self.mio_poll.registry(),
&mut self.mio_token_dispenser,
)
}
}
+86
View File
@@ -0,0 +1,86 @@
use anyhow::{Context, Result};
use heck::ToShoutySnakeCase;
use rosenpass_ciphers::{hash_domain::HashDomain, KEY_LEN};
fn calculate_hash_value(hd: HashDomain, values: &[&str]) -> Result<[u8; KEY_LEN]> {
match values.split_first() {
Some((head, tail)) => calculate_hash_value(hd.mix(head.as_bytes())?, tail),
None => Ok(hd.into_value()),
}
}
fn print_literal(path: &[&str]) -> Result<()> {
let val = calculate_hash_value(HashDomain::zero(), path)?;
let (last, prefix) = path.split_last().context("developer error!")?;
let var_name = last.to_shouty_snake_case();
print!("// hash domain hash of: ");
for n in prefix.iter() {
print!("{n} -> ");
}
println!("{last}");
let c = hex::encode(val)
.chars()
.collect::<Vec<char>>()
.chunks_exact(4)
.map(|chunk| chunk.iter().collect::<String>())
.collect::<Vec<_>>();
println!("const {var_name} : RawMsgType = RawMsgType::from_le_bytes(hex!(\"{} {} {} {} {} {} {} {}\"));",
c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]);
Ok(())
}
#[derive(Debug, Clone)]
enum Tree {
Branch(String, Vec<Tree>),
Leaf(String),
}
impl Tree {
fn name(&self) -> &str {
match self {
Self::Branch(name, _) => name,
Self::Leaf(name) => name,
}
}
fn gen_code_inner(&self, prefix: &[&str]) -> Result<()> {
let mut path = prefix.to_owned();
path.push(self.name());
match self {
Self::Branch(_, ref children) => {
for c in children.iter() {
c.gen_code_inner(&path)?
}
}
Self::Leaf(_) => print_literal(&path)?,
};
Ok(())
}
fn gen_code(&self) -> Result<()> {
self.gen_code_inner(&[])
}
}
fn main() -> Result<()> {
let tree = Tree::Branch(
"Rosenpass IPC API".to_owned(),
vec![Tree::Branch(
"Rosenpass Protocol Server".to_owned(),
vec![
Tree::Leaf("Ping Request".to_owned()),
Tree::Leaf("Ping Response".to_owned()),
],
)],
);
println!("type RawMsgType = u128;");
println!();
tree.gen_code()
}
+43 -23
View File
@@ -3,9 +3,6 @@ use clap::{Parser, Subcommand};
use rosenpass_cipher_traits::Kem;
use rosenpass_ciphers::kem::StaticKem;
use rosenpass_secret_memory::file::StoreSecret;
use rosenpass_secret_memory::{
secret_policy_try_use_memfd_secrets, secret_policy_use_only_malloc_secrets,
};
use rosenpass_util::file::{LoadValue, LoadValueB64, StoreValue};
use rosenpass_wireguard_broker::brokers::native_unix::{
NativeUnixBroker, NativeUnixBrokerConfigBaseBuilder, NativeUnixBrokerConfigBaseBuilderError,
@@ -35,11 +32,21 @@ pub struct CliArgs {
#[arg(short, long, group = "log-level")]
quiet: bool,
#[command(flatten)]
#[cfg(feature = "experiment_api")]
api: crate::api::cli::ApiCli,
#[command(subcommand)]
pub command: CliCommand,
}
impl CliArgs {
pub fn apply_to_config(&self, _cfg: &mut config::Rosenpass) -> anyhow::Result<()> {
#[cfg(feature = "experiment_api")]
self.api.apply_to_config(_cfg)?;
Ok(())
}
/// returns the log level filter set by CLI args
/// returns `None` if the user did not specify any log level filter via CLI
///
@@ -152,21 +159,14 @@ pub enum CliCommand {
Man,
}
impl CliCommand {
impl CliArgs {
/// runs the command specified via CLI
///
/// ## TODO
/// - This method consumes the [`CliCommand`] value. It might be wise to use a reference...
pub fn run(self, test_helpers: Option<AppServerTest>) -> anyhow::Result<()> {
//Specify secret policy
#[cfg(feature = "enable_memfd_alloc")]
secret_policy_try_use_memfd_secrets();
#[cfg(not(feature = "enable_memfd_alloc"))]
secret_policy_use_only_malloc_secrets();
use CliCommand::*;
match self {
match &self.command {
Man => {
let man_cmd = std::process::Command::new("man")
.args(["1", "rosenpass"])
@@ -178,7 +178,7 @@ impl CliCommand {
}
GenConfig { config_file, force } => {
ensure!(
force || !config_file.exists(),
*force || !config_file.exists(),
"config file {config_file:?} already exists"
);
@@ -193,9 +193,9 @@ impl CliCommand {
let mut secret_key: Option<PathBuf> = None;
// Manual arg parsing, since clap wants to prefix flags with "--"
let mut args = args.into_iter();
let mut args = args.iter();
loop {
match (args.next().as_deref(), args.next()) {
match (args.next().map(|x| x.as_str()), args.next()) {
(Some("private-key"), Some(opt)) | (Some("secret-key"), Some(opt)) => {
secret_key = Some(opt.into());
}
@@ -237,7 +237,7 @@ impl CliCommand {
(config.public_key, config.secret_key)
}
(_, Some(pkf), Some(skf)) => (pkf, skf),
(_, Some(pkf), Some(skf)) => (pkf.clone(), skf.clone()),
_ => {
bail!("either a config-file or both public-key and secret-key file are required")
}
@@ -269,31 +269,36 @@ impl CliCommand {
"config file '{config_file:?}' does not exist"
);
let config = config::Rosenpass::load(config_file)?;
let mut config = config::Rosenpass::load(config_file)?;
config.validate()?;
self.apply_to_config(&mut config)?;
Self::event_loop(config, test_helpers)?;
}
Exchange {
first_arg,
mut rest_of_args,
rest_of_args,
config_file,
} => {
rest_of_args.insert(0, first_arg);
let mut rest_of_args = rest_of_args.clone();
rest_of_args.insert(0, first_arg.clone());
let args = rest_of_args;
let mut config = config::Rosenpass::parse_args(args)?;
if let Some(p) = config_file {
config.store(&p)?;
config.config_file_path = p;
config.store(p)?;
config.config_file_path.clone_from(p);
}
config.validate()?;
self.apply_to_config(&mut config)?;
Self::event_loop(config, test_helpers)?;
}
Validate { config_files } => {
for file in config_files {
match config::Rosenpass::load(&file) {
match config::Rosenpass::load(file) {
Ok(config) => {
eprintln!("{file:?} is valid TOML and conforms to the expected schema");
match config.validate() {
@@ -315,6 +320,7 @@ impl CliCommand {
test_helpers: Option<AppServerTest>,
) -> anyhow::Result<()> {
const MAX_PSK_SIZE: usize = 1000;
// load own keys
let sk = SSk::load(&config.secret_key)?;
let pk = SPk::load(&config.public_key)?;
@@ -323,11 +329,13 @@ impl CliCommand {
let mut srv = std::boxed::Box::<AppServer>::new(AppServer::new(
sk,
pk,
config.listen,
config.listen.clone(),
config.verbosity,
test_helpers,
)?);
config.apply_to_app_server(&mut srv)?;
let broker_store_ptr = srv.register_broker(Box::new(NativeUnixBroker::new()))?;
fn cfg_err_map(e: NativeUnixBrokerConfigBaseBuilderError) -> anyhow::Error {
@@ -375,3 +383,15 @@ fn generate_and_save_keypair(secret_key: PathBuf, public_key: PathBuf) -> anyhow
ssk.store_secret(secret_key)?;
spk.store(public_key)
}
#[cfg(feature = "internal_testing")]
pub mod testing {
use super::*;
pub fn generate_and_save_keypair(
secret_key: PathBuf,
public_key: PathBuf,
) -> anyhow::Result<()> {
super::generate_and_save_keypair(secret_key, public_key)
}
}
+15 -1
View File
@@ -19,6 +19,8 @@ use anyhow::{bail, ensure};
use rosenpass_util::file::{fopen_w, Visibility};
use serde::{Deserialize, Serialize};
use crate::app_server::AppServer;
#[derive(Debug, Serialize, Deserialize)]
pub struct Rosenpass {
/// path to the public key file
@@ -27,6 +29,10 @@ pub struct Rosenpass {
/// path to the secret key file
pub secret_key: PathBuf,
/// Location of the API listen sockets
#[cfg(feature = "experiment_api")]
pub api: crate::api::config::ApiConfig,
/// list of [`SocketAddr`] to listen on
///
/// Examples:
@@ -54,7 +60,7 @@ pub struct Rosenpass {
/// ## TODO
/// - replace this type with [`log::LevelFilter`], also see <https://github.com/rosenpass/rosenpass/pull/246>
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Copy, Clone)]
pub enum Verbosity {
Quiet,
Verbose,
@@ -157,6 +163,12 @@ impl Rosenpass {
self.store(&self.config_file_path)
}
pub fn apply_to_app_server(&self, _srv: &mut AppServer) -> anyhow::Result<()> {
#[cfg(feature = "experiment_api")]
self.api.apply_to_app_server(_srv)?;
Ok(())
}
/// Validate a configuration
///
/// ## TODO
@@ -206,6 +218,8 @@ impl Rosenpass {
public_key: PathBuf::from(public_key.as_ref()),
secret_key: PathBuf::from(secret_key.as_ref()),
listen: vec![],
#[cfg(feature = "experiment_api")]
api: crate::api::config::ApiConfig::default(),
verbosity: Verbosity::Quiet,
peers: vec![],
config_file_path: PathBuf::new(),
+6
View File
@@ -1,3 +1,5 @@
#[cfg(feature = "experiment_api")]
pub mod api;
pub mod app_server;
pub mod cli;
pub mod config;
@@ -11,4 +13,8 @@ pub enum RosenpassError {
BufferSizeMismatch,
#[error("invalid message type")]
InvalidMessageType(u8),
#[error("invalid API message type")]
InvalidApiMessageType(u128),
#[error("could not parse API message")]
InvalidApiMessage,
}
+10 -2
View File
@@ -8,6 +8,14 @@ pub fn main() {
// parse CLI arguments
let args = CliArgs::parse();
{
use rosenpass_secret_memory as SM;
#[cfg(feature = "experiment_memfd_secret")]
SM::secret_policy_try_use_memfd_secrets();
#[cfg(not(feature = "experiment_memfd_secret"))]
SM::secret_policy_use_only_malloc_secrets();
}
// init logging
{
let mut log_builder = env_logger::Builder::from_default_env(); // sets log level filter from environment (or defaults)
@@ -26,10 +34,10 @@ pub fn main() {
// error!("error dummy");
}
match args.command.run(None) {
match args.run(None) {
Ok(_) => {}
Err(e) => {
error!("{e}");
error!("{e:?}");
exit(1);
}
}
+180
View File
@@ -0,0 +1,180 @@
use std::{
io::{BufRead, BufReader},
net::ToSocketAddrs,
os::unix::net::UnixStream,
process::Stdio,
};
use anyhow::{bail, Context};
use rosenpass::api;
use rosenpass_to::{ops::copy_slice_least_src, To};
use rosenpass_util::zerocopy::ZerocopySliceExt;
use rosenpass_util::{
file::LoadValueB64,
length_prefix_encoding::{decoder::LengthPrefixDecoder, encoder::LengthPrefixEncoder},
};
use tempfile::TempDir;
use zerocopy::AsBytes;
use rosenpass::protocol::SymKey;
#[test]
fn api_integration_test() -> anyhow::Result<()> {
rosenpass_secret_memory::policy::secret_policy_use_only_malloc_secrets();
let dir = TempDir::with_prefix("rosenpass-api-integration-test")?;
macro_rules! tempfile {
($($lst:expr),+) => {{
let mut buf = dir.path().to_path_buf();
$(buf.push($lst);)*
buf
}}
}
let peer_a_endpoint = "[::1]:61423";
let peer_a_osk = tempfile!("a.osk");
let peer_b_osk = tempfile!("b.osk");
use rosenpass::config;
let peer_a = config::Rosenpass {
config_file_path: tempfile!("a.config"),
secret_key: tempfile!("a.sk"),
public_key: tempfile!("a.pk"),
listen: peer_a_endpoint.to_socket_addrs()?.collect(), // TODO: This could collide by accident
verbosity: config::Verbosity::Verbose,
api: api::config::ApiConfig {
listen_path: vec![tempfile!("a.sock")],
listen_fd: vec![],
stream_fd: vec![],
},
peers: vec![config::RosenpassPeer {
public_key: tempfile!("b.pk"),
key_out: Some(peer_a_osk.clone()),
endpoint: None,
pre_shared_key: None,
wg: None,
}],
};
let peer_b = config::Rosenpass {
config_file_path: tempfile!("b.config"),
secret_key: tempfile!("b.sk"),
public_key: tempfile!("b.pk"),
listen: vec![],
verbosity: config::Verbosity::Verbose,
api: api::config::ApiConfig {
listen_path: vec![tempfile!("b.sock")],
listen_fd: vec![],
stream_fd: vec![],
},
peers: vec![config::RosenpassPeer {
public_key: tempfile!("a.pk"),
key_out: Some(peer_b_osk.clone()),
endpoint: Some(peer_a_endpoint.to_owned()),
pre_shared_key: None,
wg: None,
}],
};
// Generate the keys
rosenpass::cli::testing::generate_and_save_keypair(
peer_a.secret_key.clone(),
peer_a.public_key.clone(),
)?;
rosenpass::cli::testing::generate_and_save_keypair(
peer_b.secret_key.clone(),
peer_b.public_key.clone(),
)?;
// Write the configuration files
peer_a.commit()?;
peer_b.commit()?;
// Start peer a
let proc_a = std::process::Command::new(env!("CARGO_BIN_EXE_rosenpass"))
.args([
"exchange-config",
peer_a.config_file_path.to_str().context("")?,
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.spawn()?;
// Start peer b
let proc_b = std::process::Command::new(env!("CARGO_BIN_EXE_rosenpass"))
.args([
"exchange-config",
peer_b.config_file_path.to_str().context("")?,
])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.spawn()?;
// Acquire stdout
let mut out_a = BufReader::new(proc_a.stdout.context("")?).lines();
let mut out_b = BufReader::new(proc_b.stdout.context("")?).lines();
// Wait for the keys to successfully exchange a key
let mut attempt = 0;
loop {
let line_a = out_a.next().context("")??;
let line_b = out_b.next().context("")??;
let words_a = line_a.split(' ').collect::<Vec<_>>();
let words_b = line_b.split(' ').collect::<Vec<_>>();
// FIXED FIXED PEER-ID FIXED FILENAME STATUS
// output-key peer KZqXTZ4l2aNnkJtLPhs4D8JxHTGmRSL9w3Qr+X8JxFk= key-file "client-A-osk" exchanged
let peer_a_id = words_b
.get(2)
.with_context(|| format!("Bad rosenpass output: `{line_b}`"))?;
let peer_b_id = words_a
.get(2)
.with_context(|| format!("Bad rosenpass output: `{line_a}`"))?;
assert_eq!(
line_a,
format!(
"output-key peer {peer_b_id} key-file \"{}\" exchanged",
peer_a_osk.to_str().context("")?
)
);
assert_eq!(
line_b,
format!(
"output-key peer {peer_a_id} key-file \"{}\" exchanged",
peer_b_osk.to_str().context("")?
)
);
// Read OSKs
let osk_a = SymKey::load_b64::<64, _>(peer_a_osk.clone())?;
let osk_b = SymKey::load_b64::<64, _>(peer_b_osk.clone())?;
match osk_a.secret() == osk_b.secret() {
true => break,
false if attempt > 10 => bail!("Peers did not produce a matching key even after ten attempts. Something is wrong with the key exchange!"),
false => {},
};
attempt += 1;
}
// Now connect to the peers
let api_a = UnixStream::connect(&peer_a.api.listen_path[0])?;
let api_b = UnixStream::connect(&peer_b.api.listen_path[0])?;
for conn in ([api_a, api_b]).iter() {
let mut echo = [0u8; 256];
copy_slice_least_src("Hello World".as_bytes()).to(&mut echo);
let req = api::PingRequest::new(echo);
LengthPrefixEncoder::from_message(req.as_bytes()).write_all_to_stdio(conn)?;
let mut decoder = LengthPrefixDecoder::new([0u8; api::MAX_RESPONSE_LEN]);
let res = decoder.read_all_from_stdio(conn)?;
let res = res.zk_parse::<api::PingResponse>()?;
assert_eq!(*res, api::PingResponse::new(echo));
}
Ok(())
}
+22 -15
View File
@@ -15,9 +15,19 @@ use std::io::Write;
const BIN: &str = "rosenpass";
fn setup_tests() {
use rosenpass_secret_memory as SM;
#[cfg(feature = "experiment_memfd_secret")]
SM::secret_policy_try_use_memfd_secrets();
#[cfg(not(feature = "experiment_memfd_secret"))]
SM::secret_policy_use_only_malloc_secrets();
}
// check that we can generate keys
#[test]
fn generate_keys() {
setup_tests();
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("keygen");
fs::create_dir_all(&tmpdir).unwrap();
@@ -94,14 +104,11 @@ fn run_server_client_exchange(
.unwrap();
std::thread::spawn(move || {
cli.command
.run(Some(
server_test_builder
.termination_handler(Some(server_terminate_rx))
.build()
.unwrap(),
))
let test_helpers = server_test_builder
.termination_handler(Some(server_terminate_rx))
.build()
.unwrap();
cli.run(Some(test_helpers)).unwrap();
});
let cli = CliArgs::try_parse_from(
@@ -112,14 +119,11 @@ fn run_server_client_exchange(
.unwrap();
std::thread::spawn(move || {
cli.command
.run(Some(
client_test_builder
.termination_handler(Some(client_terminate_rx))
.build()
.unwrap(),
))
let test_helpers = client_test_builder
.termination_handler(Some(client_terminate_rx))
.build()
.unwrap();
cli.run(Some(test_helpers)).unwrap();
});
// give them some time to do the key exchange under load
@@ -134,6 +138,7 @@ fn run_server_client_exchange(
#[test]
#[serial]
fn check_exchange_under_normal() {
setup_tests();
setup_logging();
let tmpdir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("exchange");
@@ -206,6 +211,7 @@ fn check_exchange_under_normal() {
#[test]
#[serial]
fn check_exchange_under_dos() {
setup_tests();
setup_logging();
//Generate binary with responder with feature integration_test
@@ -283,6 +289,7 @@ struct MockBrokerInner {
interface: Option<String>,
}
#[allow(dead_code)]
#[derive(Debug, Default)]
struct MockBroker {
inner: Arc<Mutex<MockBrokerInner>>,
@@ -321,7 +328,7 @@ impl rosenpass_wireguard_broker::WireGuardBroker for MockBroker {
if let Ok(ref mut mutex) = lock {
**mutex = MockBrokerInner {
psk: Some(config.psk.clone()),
peer_id: Some(config.peer_id.clone()),
peer_id: Some(*config.peer_id),
interface: Some(std::str::from_utf8(config.interface).unwrap().to_string()),
};
break Ok(());
+1 -1
View File
@@ -39,5 +39,5 @@ tempfile = {workspace = true}
stacker = {workspace = true}
[features]
enable_memfd_alloc = []
experiment_memfd_secret = []
experiment_libcrux = ["rosenpass-ciphers/experiment_libcrux"]
+2 -2
View File
@@ -11,9 +11,9 @@ mod key;
#[tokio::main]
async fn main() {
#[cfg(feature = "enable_memfd_alloc")]
#[cfg(feature = "experiment_memfd_secret")]
policy::secret_policy_try_use_memfd_secrets();
#[cfg(not(feature = "enable_memfd_alloc"))]
#[cfg(not(feature = "experiment_memfd_secret"))]
policy::secret_policy_use_only_malloc_secrets();
let cli = match Cli::parse(std::env::args().peekable()) {
+2 -1
View File
@@ -317,6 +317,7 @@ impl<const N: usize> StoreValueB64Writer for PublicBox<N> {
mod tests {
#[cfg(test)]
#[allow(clippy::module_inception)]
mod tests {
use crate::{Public, PublicBox};
use rosenpass_util::{
@@ -346,7 +347,7 @@ mod tests {
// Store the original bytes to an example file in the temporary directory
let example_file = temp_dir.path().join("example_file");
std::fs::write(example_file.clone(), &original_bytes).unwrap();
std::fs::write(&example_file, original_bytes).unwrap();
// Load the value from the example file into our generic type
let loaded_public = T::load(&example_file).unwrap();
+1 -2
View File
@@ -1,6 +1,5 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::convert::TryInto;
use std::fmt;
use std::ops::{Deref, DerefMut};
use std::path::Path;
@@ -387,7 +386,7 @@ mod test {
// Store the original secret to an example file in the temporary directory
let example_file = temp_dir.path().join("example_file");
std::fs::write(example_file.clone(), &original_bytes).unwrap();
std::fs::write(&example_file, original_bytes).unwrap();
// Load the secret from the example file
let loaded_secret = Secret::load(&example_file).unwrap();
+2 -2
View File
@@ -6,8 +6,8 @@
//! - `Dst: ?Sized`; (e.g. [u8]) The target to write to
//! - `Out: Sized = &mut Dst`; (e.g. &mut [u8]) A reference to the target to write to
//! - `Coercable: ?Sized + DstCoercion<Dst>`; (e.g. `[u8]`, `[u8; 16]`) Some value that
//! destination coercion can be applied to. Usually either `Dst` itself (e.g. `[u8]` or some sized variant of
//! `Dst` (e.g. `[u8; 64]`).
//! destination coercion can be applied to. Usually either `Dst` itself (e.g. `[u8]` or some sized variant of
//! `Dst` (e.g. `[u8; 64]`).
//! - `Ret: Sized`; (anything) must be `CondenseBeside<_>` if condensing is to be applied. The ordinary return value of a function with an output
//! - `Val: Sized + BorrowMut<Dst>`; (e.g. [u8; 16]) Some owned storage that can be borrowed as `Dst`
//! - `Condensed: Sized = CondenseBeside<Val>::Condensed`; (e.g. [u8; 16], Result<[u8; 16]>) The combiation of Val and Ret after condensing was applied (`Beside<Val, Ret>::condense()`/`Ret::condense(v)` for all `v : Val`).
+3
View File
@@ -18,3 +18,6 @@ typenum = { workspace = true }
static_assertions = { workspace = true }
rustix = {workspace = true}
zeroize = {workspace = true}
zerocopy = { workspace = true }
thiserror = { workspace = true }
mio = { workspace = true }
+47 -9
View File
@@ -1,12 +1,50 @@
use std::os::fd::{OwnedFd, RawFd};
use rustix::{
fd::{AsFd, BorrowedFd, FromRawFd, OwnedFd, RawFd},
io::{fcntl_dupfd_cloexec, DupFlags},
};
/// Clone some file descriptor
use crate::mem::Forgetting;
/// Prepare a file descriptor for use in Rust code.
///
/// If the file descriptor is invalid, an error will be raised.
pub fn claim_fd(fd: RawFd) -> anyhow::Result<OwnedFd> {
use rustix::{fd::BorrowedFd, io::dup};
// This is safe since [dup] will simply raise
let fd = unsafe { dup(BorrowedFd::borrow_raw(fd))? };
Ok(fd)
/// Checks if the file descriptor is valid and duplicates it to a new file descriptor.
/// The old file descriptor is masked to avoid potential use after free (on file descriptor)
/// in case the given file descriptor is still used somewhere
pub fn claim_fd(fd: RawFd) -> rustix::io::Result<OwnedFd> {
let new = clone_fd_cloexec(unsafe { BorrowedFd::borrow_raw(fd) })?;
mask_fd(fd)?;
Ok(new)
}
pub fn mask_fd(fd: RawFd) -> rustix::io::Result<()> {
// Safety: because the OwnedFd resulting from OwnedFd::from_raw_fd is wrapped in a Forgetting,
// it never gets dropped, meaning that fd is never closed and thus outlives the OwnedFd
let mut owned = Forgetting::new(unsafe { OwnedFd::from_raw_fd(fd) });
clone_fd_to_cloexec(open_nullfd()?, &mut owned)
}
pub fn clone_fd_cloexec<Fd: AsFd>(fd: Fd) -> rustix::io::Result<OwnedFd> {
const MINFD: RawFd = 3; // Avoid stdin, stdout, and stderr
fcntl_dupfd_cloexec(fd, MINFD)
}
#[cfg(target_os = "linux")]
pub fn clone_fd_to_cloexec<Fd: AsFd>(fd: Fd, new: &mut OwnedFd) -> rustix::io::Result<()> {
use rustix::io::dup3;
dup3(fd, new, DupFlags::CLOEXEC)
}
#[cfg(not(target_os = "linux"))]
pub fn clone_fd_to_cloexec<Fd: AsFd>(fd: Fd, new: &mut OwnedFd) -> rustix::io::Result<()> {
use rustix::io::{dup2, fcntl_setfd, FdFlags};
dup2(&fd, new)?;
fcntl_setfd(&new, FdFlags::CLOEXEC)
}
/// Open a "blocked" file descriptor. I.e. a file descriptor that is neither meant for reading nor
/// writing
pub fn open_nullfd() -> rustix::io::Result<OwnedFd> {
use rustix::fs::{open, Mode, OFlags};
// TODO: Add tests showing that this will throw errors on use
open("/dev/null", OFlags::CLOEXEC, Mode::empty())
}
-1
View File
@@ -2,7 +2,6 @@ use anyhow::ensure;
use std::fs::File;
use std::io::Read;
use std::os::unix::fs::OpenOptionsExt;
use std::result::Result;
use std::{fs::OpenOptions, path::Path};
pub enum Visibility {
+4
View File
@@ -13,3 +13,7 @@ where
f(&v);
v
}
pub fn run<R, F: FnOnce() -> R>(f: F) -> R {
f()
}
+110
View File
@@ -0,0 +1,110 @@
use std::{borrow::Borrow, io};
pub trait IoErrorKind {
fn io_error_kind(&self) -> io::ErrorKind;
}
impl<T: Borrow<io::Error>> IoErrorKind for T {
fn io_error_kind(&self) -> io::ErrorKind {
self.borrow().kind()
}
}
pub trait TryIoErrorKind {
fn try_io_error_kind(&self) -> Option<io::ErrorKind>;
}
impl<T: IoErrorKind> TryIoErrorKind for T {
fn try_io_error_kind(&self) -> Option<io::ErrorKind> {
Some(self.io_error_kind())
}
}
pub trait IoResultKindHintExt<T>: Sized {
type Error;
fn io_err_kind_hint(self) -> Result<T, (Self::Error, io::ErrorKind)>;
}
impl<T, E: IoErrorKind> IoResultKindHintExt<T> for Result<T, E> {
type Error = E;
fn io_err_kind_hint(self) -> Result<T, (E, io::ErrorKind)> {
self.map_err(|e| {
let kind = e.borrow().io_error_kind();
(e, kind)
})
}
}
pub trait TryIoResultKindHintExt<T>: Sized {
type Error;
fn try_io_err_kind_hint(self) -> Result<T, (Self::Error, Option<io::ErrorKind>)>;
}
impl<T, E: TryIoErrorKind> TryIoResultKindHintExt<T> for Result<T, E> {
type Error = E;
fn try_io_err_kind_hint(self) -> Result<T, (E, Option<io::ErrorKind>)> {
self.map_err(|e| {
let opt_kind = e.borrow().try_io_error_kind();
(e, opt_kind)
})
}
}
/// Automatically handles `std::io::ErrorKind::Interrupted`.
///
/// - If there is no error (i.e. on `Ok(r)`), the function will return `Ok(Some(r))`
/// - `Interrupted` is handled internally, by retrying the IO operation
/// - Other errors are returned as is
pub fn handle_interrupted<R, E, F>(mut iofn: F) -> Result<Option<R>, E>
where
E: TryIoErrorKind,
F: FnMut() -> Result<R, E>,
{
use io::ErrorKind as E;
loop {
match iofn().try_io_err_kind_hint() {
Ok(v) => return Ok(Some(v)),
Err((_, Some(E::Interrupted))) => continue, // try again
Err((e, _)) => return Err(e),
};
}
}
/// Automatically handles `std::io::ErrorKind::{WouldBlock, Interrupted}`.
///
/// - If there is no error (i.e. on `Ok(r)`), the function will return `Ok(Some(r))`
/// - `Interrupted` is handled internally, by retrying the IO operation
/// - `WouldBlock` is handled by returning `Ok(None)`,
/// - Other errors are returned as is
pub fn nonblocking_handle_io_errors<R, E, F>(mut iofn: F) -> Result<Option<R>, E>
where
E: TryIoErrorKind,
F: FnMut() -> Result<R, E>,
{
use io::ErrorKind as E;
loop {
match iofn().try_io_err_kind_hint() {
Ok(v) => return Ok(Some(v)),
Err((_, Some(E::WouldBlock))) => return Ok(None), // no data to read
Err((_, Some(E::Interrupted))) => continue, // try again
Err((e, _)) => return Err(e),
};
}
}
pub trait ReadNonblockingWithBoringErrorsHandledExt {
/// Convenience wrapper using [nonblocking_handle_io_errors] with [std::io::Read]
fn read_nonblocking_with_boring_errors_handled(
&mut self,
buf: &mut [u8],
) -> io::Result<Option<usize>>;
}
impl<T: io::Read> ReadNonblockingWithBoringErrorsHandledExt for T {
fn read_nonblocking_with_boring_errors_handled(
&mut self,
buf: &mut [u8],
) -> io::Result<Option<usize>> {
nonblocking_handle_io_errors(|| self.read(buf))
}
}
+359
View File
@@ -0,0 +1,359 @@
use std::{borrow::BorrowMut, cmp::min, io};
use thiserror::Error;
use zeroize::Zeroize;
use crate::{
io::{TryIoErrorKind, TryIoResultKindHintExt},
result::ensure_or,
};
pub const HEADER_SIZE: usize = std::mem::size_of::<u64>();
#[derive(Error, Debug)]
pub enum SanityError {
#[error("Offset is out of read buffer bounds")]
OutOfBufferBounds,
#[error("Offset is out of message buffer bounds")]
OutOfMessageBounds,
}
#[derive(Error, Debug)]
#[error("Message too large ({msg_size} bytes) for buffer ({buf_size} bytes)")]
pub struct MessageTooLargeError {
msg_size: usize,
buf_size: usize,
}
impl MessageTooLargeError {
pub fn new(msg_size: usize, buf_size: usize) -> Self {
Self { msg_size, buf_size }
}
pub fn ensure(msg_size: usize, buf_size: usize) -> Result<(), Self> {
let err = MessageTooLargeError { msg_size, buf_size };
ensure_or(msg_size <= buf_size, err)
}
}
#[derive(Debug)]
pub struct ReadFromIoReturn<'a> {
pub bytes_read: usize,
pub message: Option<&'a mut [u8]>,
}
impl<'a> ReadFromIoReturn<'a> {
pub fn new(bytes_read: usize, message: Option<&'a mut [u8]>) -> Self {
Self {
bytes_read,
message,
}
}
}
#[derive(Debug, Error)]
pub enum ReadFromIoError {
#[error("Error reading from the underlying stream")]
IoError(#[from] io::Error),
#[error("Message size out of buffer bounds")]
MessageTooLargeError(#[from] MessageTooLargeError),
}
impl TryIoErrorKind for ReadFromIoError {
fn try_io_error_kind(&self) -> Option<io::ErrorKind> {
match self {
ReadFromIoError::IoError(ioe) => Some(ioe.kind()),
_ => None,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct LengthPrefixDecoder<Buf: BorrowMut<[u8]>> {
header: [u8; HEADER_SIZE],
buf: Buf,
off: usize,
}
impl<Buf: BorrowMut<[u8]>> LengthPrefixDecoder<Buf> {
pub fn new(buf: Buf) -> Self {
let header = Default::default();
let off = 0;
Self { header, buf, off }
}
pub fn clear(&mut self) {
self.zeroize()
}
pub fn from_parts(header: [u8; HEADER_SIZE], buf: Buf, off: usize) -> Self {
Self { header, buf, off }
}
pub fn into_parts(self) -> ([u8; HEADER_SIZE], Buf, usize) {
let Self { header, buf, off } = self;
(header, buf, off)
}
pub fn read_all_from_stdio<R: io::Read>(
&mut self,
mut r: R,
) -> Result<&mut [u8], ReadFromIoError> {
use io::ErrorKind as K;
loop {
match self.read_from_stdio(&mut r).try_io_err_kind_hint() {
// Success (appeasing the borrow checker by calling message_mut())
Ok(ReadFromIoReturn {
message: Some(_), ..
}) => break Ok(self.message_mut().unwrap().unwrap()),
// Unexpected EOF
Ok(ReadFromIoReturn { bytes_read: 0, .. }) => {
break Err(ReadFromIoError::IoError(io::Error::new(
K::UnexpectedEof,
"",
)))
}
// Retry
Ok(ReadFromIoReturn { message: None, .. }) => continue,
Err((_, Some(K::Interrupted))) => continue,
// Other error
Err((e, _)) => break Err(e),
}
}
}
pub fn read_from_stdio<R: io::Read>(
&mut self,
mut r: R,
) -> Result<ReadFromIoReturn, ReadFromIoError> {
Ok(match self.next_slice_to_write_to()? {
// Read some bytes; any MessageTooLargeError in the call to self.message_mut() is
// ignored to ensure this function changes no state upon errors; the user should rerun
// the function and collect the MessageTooLargeError on the following invocation
Some(buf) => {
let bytes_read = r.read(buf)?;
self.advance(bytes_read).unwrap();
let message = self.message_mut().ok().flatten();
ReadFromIoReturn {
bytes_read,
message,
}
}
// Message is already fully read; full delegation to self.message_mut()
None => ReadFromIoReturn {
bytes_read: 0,
message: self.message_mut()?,
},
})
}
pub fn next_slice_to_write_to(&mut self) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
fn some_if_nonempty(buf: &mut [u8]) -> Option<&mut [u8]> {
match buf.is_empty() {
true => None,
false => Some(buf),
}
}
macro_rules! return_if_nonempty_some {
($opt:expr) => {{
// Deliberate double expansion of $opt to appease the borrow checker *sigh*
if $opt.and_then(some_if_nonempty).is_some() {
return Ok($opt);
}
}};
}
return_if_nonempty_some!(Some(self.header_buffer_left_mut()));
return_if_nonempty_some!(self.message_fragment_left_mut()?);
Ok(None)
}
pub fn advance(&mut self, count: usize) -> Result<(), SanityError> {
let off = self.off + count;
let msg_off = off.saturating_sub(HEADER_SIZE);
use SanityError as E;
let alloc = self.message_buffer().len();
let msgsz = self.message_size();
ensure_or(msg_off <= alloc, E::OutOfBufferBounds)?;
ensure_or(
msgsz.map(|s| msg_off <= s).unwrap_or(true),
E::OutOfMessageBounds,
)?;
self.off = off;
Ok(())
}
pub fn ensure_sufficient_msg_buffer(&self) -> Result<(), MessageTooLargeError> {
let buf_size = self.message_buffer().len();
let msg_size = match self.get_header() {
None => return Ok(()),
Some(v) => v,
};
MessageTooLargeError::ensure(msg_size, buf_size)
}
pub fn header_buffer(&self) -> &[u8] {
&self.header[..]
}
pub fn header_buffer_mut(&mut self) -> &mut [u8] {
&mut self.header[..]
}
pub fn message_buffer(&self) -> &[u8] {
self.buf.borrow()
}
pub fn message_buffer_mut(&mut self) -> &mut [u8] {
self.buf.borrow_mut()
}
pub fn bytes_read(&self) -> &usize {
&self.off
}
pub fn into_message_buffer(self) -> Buf {
let Self { buf, .. } = self;
buf
}
pub fn header_buffer_offset(&self) -> usize {
min(self.off, HEADER_SIZE)
}
pub fn message_buffer_offset(&self) -> usize {
self.off.saturating_sub(HEADER_SIZE)
}
pub fn has_header(&self) -> bool {
self.header_buffer_offset() == HEADER_SIZE
}
pub fn has_message(&self) -> Result<bool, MessageTooLargeError> {
self.ensure_sufficient_msg_buffer()?;
let msg_size = match self.get_header() {
None => return Ok(false),
Some(v) => v,
};
Ok(self.message_buffer_avail().len() == msg_size)
}
pub fn header_buffer_avail(&self) -> &[u8] {
let off = self.header_buffer_offset();
&self.header_buffer()[..off]
}
pub fn header_buffer_avail_mut(&mut self) -> &mut [u8] {
let off = self.header_buffer_offset();
&mut self.header_buffer_mut()[..off]
}
pub fn header_buffer_left(&self) -> &[u8] {
let off = self.header_buffer_offset();
&self.header_buffer()[off..]
}
pub fn header_buffer_left_mut(&mut self) -> &mut [u8] {
let off = self.header_buffer_offset();
&mut self.header_buffer_mut()[off..]
}
pub fn message_buffer_avail(&self) -> &[u8] {
let off = self.message_buffer_offset();
&self.message_buffer()[..off]
}
pub fn message_buffer_avail_mut(&mut self) -> &mut [u8] {
let off = self.message_buffer_offset();
&mut self.message_buffer_mut()[..off]
}
pub fn message_buffer_left(&self) -> &[u8] {
let off = self.message_buffer_offset();
&self.message_buffer()[off..]
}
pub fn message_buffer_left_mut(&mut self) -> &mut [u8] {
let off = self.message_buffer_offset();
&mut self.message_buffer_mut()[off..]
}
pub fn get_header(&self) -> Option<usize> {
match self.header_buffer_offset() == HEADER_SIZE {
false => None,
true => Some(u64::from_le_bytes(self.header) as usize),
}
}
pub fn message_size(&self) -> Option<usize> {
self.get_header()
}
pub fn encoded_message_bytes(&self) -> Option<usize> {
self.message_size().map(|sz| sz + HEADER_SIZE)
}
pub fn message_fragment(&self) -> Result<Option<&[u8]>, MessageTooLargeError> {
self.ensure_sufficient_msg_buffer()?;
Ok(self.message_size().map(|sz| &self.message_buffer()[..sz]))
}
pub fn message_fragment_mut(&mut self) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
self.ensure_sufficient_msg_buffer()?;
Ok(self
.message_size()
.map(|sz| &mut self.message_buffer_mut()[..sz]))
}
pub fn message_fragment_avail(&self) -> Result<Option<&[u8]>, MessageTooLargeError> {
let off = self.message_buffer_avail().len();
self.message_fragment()
.map(|frag| frag.map(|frag| &frag[..off]))
}
pub fn message_fragment_avail_mut(
&mut self,
) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
let off = self.message_buffer_avail().len();
self.message_fragment_mut()
.map(|frag| frag.map(|frag| &mut frag[..off]))
}
pub fn message_fragment_left(&self) -> Result<Option<&[u8]>, MessageTooLargeError> {
let off = self.message_buffer_avail().len();
self.message_fragment()
.map(|frag| frag.map(|frag| &frag[off..]))
}
pub fn message_fragment_left_mut(&mut self) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
let off = self.message_buffer_avail().len();
self.message_fragment_mut()
.map(|frag| frag.map(|frag| &mut frag[off..]))
}
pub fn message(&self) -> Result<Option<&[u8]>, MessageTooLargeError> {
let sz = self.message_size();
self.message_fragment_avail()
.map(|frag_opt| frag_opt.and_then(|frag| (frag.len() == sz?).then_some(frag)))
}
pub fn message_mut(&mut self) -> Result<Option<&mut [u8]>, MessageTooLargeError> {
let sz = self.message_size();
self.message_fragment_avail_mut()
.map(|frag_opt| frag_opt.and_then(|frag| (frag.len() == sz?).then_some(frag)))
}
}
impl<Buf: BorrowMut<[u8]>> Zeroize for LengthPrefixDecoder<Buf> {
fn zeroize(&mut self) {
self.header.zeroize();
self.message_buffer_mut().zeroize();
self.off.zeroize();
}
}
+381
View File
@@ -0,0 +1,381 @@
use std::{
borrow::{Borrow, BorrowMut},
cmp::min,
io,
};
use thiserror::Error;
use zeroize::Zeroize;
use crate::{io::IoResultKindHintExt, result::ensure_or};
pub const HEADER_SIZE: usize = std::mem::size_of::<u64>();
#[derive(Error, Debug, Clone, Copy)]
#[error("Write position is out of buffer bounds")]
pub struct PositionOutOfBufferBounds;
#[derive(Error, Debug, Clone, Copy)]
#[error("Write position is out of message bounds")]
pub struct PositionOutOfMessageBounds;
#[derive(Error, Debug, Clone, Copy)]
#[error("Write position is out of header bounds")]
pub struct PositionOutOfHeaderBounds;
#[derive(Error, Debug, Clone, Copy)]
#[error("Message length is bigger than buffer length")]
pub struct MessageTooLarge;
#[derive(Error, Debug, Clone, Copy)]
pub enum MessageLenSanityError {
#[error("{0:?}")]
PositionOutOfMessageBounds(#[from] PositionOutOfMessageBounds),
#[error("{0:?}")]
MessageTooLarge(#[from] MessageTooLarge),
}
#[derive(Error, Debug, Clone, Copy)]
pub enum PositionSanityError {
#[error("{0:?}")]
PositionOutOfMessageBounds(#[from] PositionOutOfMessageBounds),
#[error("{0:?}")]
PositionOutOfBufferBounds(#[from] PositionOutOfBufferBounds),
}
#[derive(Error, Debug, Clone, Copy)]
pub enum SanityError {
#[error("{0:?}")]
PositionOutOfMessageBounds(#[from] PositionOutOfMessageBounds),
#[error("{0:?}")]
PositionOutOfBufferBounds(#[from] PositionOutOfBufferBounds),
#[error("{0:?}")]
MessageTooLarge(#[from] MessageTooLarge),
}
impl TryFrom<SanityError> for MessageLenSanityError {
type Error = PositionOutOfBufferBounds;
fn try_from(value: SanityError) -> Result<Self, Self::Error> {
use {MessageLenSanityError as T, SanityError as F};
match value {
F::PositionOutOfMessageBounds(e) => Ok(T::PositionOutOfMessageBounds(e)),
F::MessageTooLarge(e) => Ok(T::MessageTooLarge(e)),
F::PositionOutOfBufferBounds(e) => Err(e),
}
}
}
impl From<MessageLenSanityError> for SanityError {
fn from(value: MessageLenSanityError) -> Self {
use {MessageLenSanityError as F, SanityError as T};
match value {
F::PositionOutOfMessageBounds(e) => T::PositionOutOfMessageBounds(e),
F::MessageTooLarge(e) => T::MessageTooLarge(e),
}
}
}
impl From<PositionSanityError> for SanityError {
fn from(value: PositionSanityError) -> Self {
use {PositionSanityError as F, SanityError as T};
match value {
F::PositionOutOfBufferBounds(e) => T::PositionOutOfBufferBounds(e),
F::PositionOutOfMessageBounds(e) => T::PositionOutOfMessageBounds(e),
}
}
}
pub struct WriteToIoReturn {
pub bytes_written: usize,
pub done: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct LengthPrefixEncoder<Buf: Borrow<[u8]>> {
buf: Buf,
header: [u8; HEADER_SIZE],
pos: usize,
}
impl<Buf: Borrow<[u8]>> LengthPrefixEncoder<Buf> {
pub fn from_buffer(buf: Buf) -> Self {
let (header, pos) = ([0u8; HEADER_SIZE], 0);
let mut r = Self { buf, header, pos };
r.clear();
r
}
pub fn from_message(msg: Buf) -> Self {
let mut r = Self::from_buffer(msg);
r.restart_write_with_new_message(r.buffer_bytes().len())
.unwrap();
r
}
pub fn from_short_message(msg: Buf, len: usize) -> Result<Self, MessageLenSanityError> {
let mut r = Self::from_message(msg);
r.set_message_len(len)?;
Ok(r)
}
pub fn from_parts(buf: Buf, len: usize, pos: usize) -> Result<Self, SanityError> {
let mut r = Self::from_buffer(buf);
r.set_msg_len_and_position(len, pos)?;
Ok(r)
}
pub fn into_buffer(self) -> Buf {
let Self { buf, .. } = self;
buf
}
pub fn into_parts(self) -> (Buf, usize, usize) {
let len = self.message_len();
let pos = self.writing_position();
let buf = self.into_buffer();
(buf, len, pos)
}
pub fn clear(&mut self) {
self.set_msg_len_and_position(0, 0).unwrap();
self.set_message_offset(0).unwrap();
}
pub fn write_all_to_stdio<W: io::Write>(&mut self, mut w: W) -> io::Result<()> {
use io::ErrorKind as K;
loop {
match self.write_to_stdio(&mut w).io_err_kind_hint() {
// Done
Ok(WriteToIoReturn { done: true, .. }) => break Ok(()),
// Retry
Ok(WriteToIoReturn { done: false, .. }) => continue,
Err((_, K::Interrupted)) => continue,
Err((e, _)) => break Err(e),
}
}
}
pub fn write_to_stdio<W: io::Write>(&mut self, mut w: W) -> io::Result<WriteToIoReturn> {
if self.exhausted() {
return Ok(WriteToIoReturn {
bytes_written: 0,
done: true,
});
}
let buf = self.next_slice_to_write();
let bytes_written = w.write(buf)?;
self.advance(bytes_written).unwrap();
let done = self.exhausted();
Ok(WriteToIoReturn {
bytes_written,
done,
})
}
pub fn restart_write(&mut self) {
self.set_writing_position(0).unwrap()
}
pub fn restart_write_with_new_message(
&mut self,
len: usize,
) -> Result<(), MessageLenSanityError> {
self.set_msg_len_and_position(len, 0)
.map_err(|e| e.try_into().unwrap())
}
pub fn next_slice_to_write(&self) -> &[u8] {
let s = self.header_left();
if !s.is_empty() {
return s;
}
let s = self.message_left();
if !s.is_empty() {
return s;
}
&[]
}
pub fn exhausted(&self) -> bool {
self.next_slice_to_write().is_empty()
}
pub fn message(&self) -> &[u8] {
&self.buffer_bytes()[..self.message_len()]
}
pub fn header_written(&self) -> &[u8] {
&self.header()[..self.header_offset()]
}
pub fn header_left(&self) -> &[u8] {
&self.header()[self.header_offset()..]
}
pub fn message_written(&self) -> &[u8] {
&self.message()[..self.message_offset()]
}
pub fn message_left(&self) -> &[u8] {
&self.message()[self.message_offset()..]
}
pub fn buf(&self) -> &Buf {
&self.buf
}
pub fn buffer_bytes(&self) -> &[u8] {
self.buf().borrow()
}
pub fn decode_header(&self) -> u64 {
u64::from_le_bytes(self.header)
}
pub fn header(&self) -> &[u8; HEADER_SIZE] {
&self.header
}
pub fn message_len(&self) -> usize {
self.decode_header() as usize
}
pub fn encoded_message_bytes(&self) -> usize {
self.message_len() + HEADER_SIZE
}
pub fn writing_position(&self) -> usize {
self.pos
}
pub fn header_offset(&self) -> usize {
min(self.writing_position(), HEADER_SIZE)
}
pub fn message_offset(&self) -> usize {
self.writing_position().saturating_sub(HEADER_SIZE)
}
pub fn set_header(&mut self, header: [u8; HEADER_SIZE]) -> Result<(), MessageLenSanityError> {
self.offset_transaction(|t| {
t.header = header;
t.ensure_msg_in_buf_bounds()?;
t.ensure_pos_in_msg_bounds()?;
Ok(())
})
}
pub fn encode_and_set_header(&mut self, header: u64) -> Result<(), MessageLenSanityError> {
self.set_header(header.to_le_bytes())
}
pub fn set_message_len(&mut self, len: usize) -> Result<(), MessageLenSanityError> {
self.encode_and_set_header(len as u64)
}
pub fn set_writing_position(&mut self, pos: usize) -> Result<(), PositionSanityError> {
self.offset_transaction(|t| {
t.pos = pos;
t.ensure_pos_in_buf_bounds()?;
t.ensure_pos_in_msg_bounds()?;
Ok(())
})
}
pub fn set_header_offset(&mut self, off: usize) -> Result<(), PositionOutOfHeaderBounds> {
ensure_or(off <= HEADER_SIZE, PositionOutOfHeaderBounds)?;
self.set_writing_position(off).unwrap();
Ok(())
}
pub fn set_message_offset(&mut self, off: usize) -> Result<(), PositionSanityError> {
self.set_writing_position(off + HEADER_SIZE)
}
pub fn advance(&mut self, off: usize) -> Result<(), PositionSanityError> {
self.set_writing_position(self.writing_position() + off)
}
pub fn set_msg_len_and_position(&mut self, len: usize, pos: usize) -> Result<(), SanityError> {
self.pos = 0;
self.set_message_len(len)?;
self.set_writing_position(pos)?;
Ok(())
}
fn offset_transaction<E, F>(&mut self, f: F) -> Result<(), E>
where
F: FnOnce(&mut LengthPrefixEncoder<&[u8]>) -> Result<(), E>,
{
let (header, pos) = {
let (buf, header, pos) = (self.buffer_bytes(), self.header, self.pos);
let mut tmp = LengthPrefixEncoder { buf, header, pos };
f(&mut tmp)?;
Ok((tmp.header, tmp.pos))
}?;
(self.header, self.pos) = (header, pos);
Ok(())
}
fn ensure_pos_in_buf_bounds(&self) -> Result<(), PositionOutOfBufferBounds> {
ensure_or(
self.message_offset() <= self.buffer_bytes().len(),
PositionOutOfBufferBounds,
)
}
fn ensure_pos_in_msg_bounds(&self) -> Result<(), PositionOutOfMessageBounds> {
ensure_or(
self.message_offset() <= self.message_len(),
PositionOutOfMessageBounds,
)
}
fn ensure_msg_in_buf_bounds(&self) -> Result<(), MessageTooLarge> {
ensure_or(
self.message_len() <= self.buffer_bytes().len(),
MessageTooLarge,
)
}
}
impl<Buf: BorrowMut<[u8]>> LengthPrefixEncoder<Buf> {
pub fn buf_mut(&mut self) -> &mut Buf {
&mut self.buf
}
pub fn buffer_bytes_mut(&mut self) -> &mut [u8] {
self.buf.borrow_mut()
}
pub fn message_mut(&mut self) -> &mut [u8] {
let off = self.message_len();
&mut self.buffer_bytes_mut()[..off]
}
pub fn message_written_mut(&mut self) -> &mut [u8] {
let off = self.message_offset();
&mut self.message_mut()[..off]
}
pub fn message_left_mut(&mut self) -> &mut [u8] {
let off = self.message_offset();
&mut self.message_mut()[off..]
}
}
impl<Buf: BorrowMut<[u8]>> Zeroize for LengthPrefixEncoder<Buf> {
fn zeroize(&mut self) {
self.buffer_bytes_mut().zeroize();
self.header.zeroize();
self.pos.zeroize();
self.clear();
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod decoder;
pub mod encoder;
+5
View File
@@ -4,8 +4,13 @@ pub mod b64;
pub mod fd;
pub mod file;
pub mod functional;
pub mod io;
pub mod length_prefix_encoding;
pub mod mem;
pub mod mio;
pub mod ord;
pub mod result;
pub mod time;
pub mod typenum;
pub mod zerocopy;
pub mod zeroize;
+61
View File
@@ -1,5 +1,7 @@
use std::borrow::{Borrow, BorrowMut};
use std::cmp::min;
use std::mem::{forget, swap};
use std::ops::{Deref, DerefMut};
/// Concatenate two byte arrays
// TODO: Zeroize result?
@@ -31,3 +33,62 @@ pub fn cpy_min<T: BorrowMut<[u8]> + ?Sized, F: Borrow<[u8]> + ?Sized>(src: &F, d
let len = min(src.len(), dst.len());
dst[..len].copy_from_slice(&src[..len]);
}
/// Wrapper type to inhibit calling [std::mem::Drop] when the underlying variable is freed
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Default)]
pub struct Forgetting<T> {
value: Option<T>,
}
impl<T> Forgetting<T> {
pub fn new(value: T) -> Self {
let value = Some(value);
Self { value }
}
pub fn extract(mut self) -> T {
let mut value = None;
swap(&mut value, &mut self.value);
value.unwrap()
}
}
impl<T> From<T> for Forgetting<T> {
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<T> Deref for Forgetting<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.value.as_ref().unwrap()
}
}
impl<T> DerefMut for Forgetting<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.value.as_mut().unwrap()
}
}
impl<T> Borrow<T> for Forgetting<T> {
fn borrow(&self) -> &T {
self.deref()
}
}
impl<T> BorrowMut<T> for Forgetting<T> {
fn borrow_mut(&mut self) -> &mut T {
self.deref_mut()
}
}
impl<T> Drop for Forgetting<T> {
fn drop(&mut self) {
let mut value = None;
swap(&mut self.value, &mut value);
forget(value)
}
}
+39
View File
@@ -0,0 +1,39 @@
use mio::net::{UnixListener, UnixStream};
use rustix::fd::RawFd;
use crate::fd::claim_fd;
pub mod interest {
use mio::Interest;
pub const R: Interest = Interest::READABLE;
pub const W: Interest = Interest::WRITABLE;
pub const RW: Interest = R.add(W);
}
pub trait UnixListenerExt: Sized {
fn claim_fd(fd: RawFd) -> anyhow::Result<Self>;
}
impl UnixListenerExt for UnixListener {
fn claim_fd(fd: RawFd) -> anyhow::Result<Self> {
use std::os::unix::net::UnixListener as StdUnixListener;
let sock = StdUnixListener::from(claim_fd(fd)?);
sock.set_nonblocking(true)?;
Ok(UnixListener::from_std(sock))
}
}
pub trait UnixStreamExt: Sized {
fn claim_fd(fd: RawFd) -> anyhow::Result<Self>;
}
impl UnixStreamExt for UnixStream {
fn claim_fd(fd: RawFd) -> anyhow::Result<Self> {
use std::os::unix::net::UnixStream as StdUnixStream;
let sock = StdUnixStream::from(claim_fd(fd)?);
sock.set_nonblocking(true)?;
Ok(UnixStream::from_std(sock))
}
}
+11 -1
View File
@@ -1,5 +1,4 @@
use std::convert::Infallible;
use std::result::Result;
/// Try block basically…returns a result and allows the use of the question mark operator inside
#[macro_export]
@@ -97,3 +96,14 @@ impl<T> GuaranteedValue for Guaranteed<T> {
self.unwrap()
}
}
pub fn ensure_or<E>(b: bool, err: E) -> Result<(), E> {
match b {
true => Ok(()),
false => Err(err),
}
}
pub fn bail_if<E>(b: bool, err: E) -> Result<(), E> {
ensure_or(!b, err)
}
+7
View File
@@ -19,15 +19,22 @@ pub trait IntoConst<T> {
const VALUE: T;
}
#[allow(dead_code)]
struct ConstApplyNegSign<T: AssociatedUnsigned, Param: IntoConst<<T as AssociatedUnsigned>::Type>>(
*const T,
*const Param,
);
#[allow(dead_code)]
struct ConstApplyPosSign<T: AssociatedUnsigned, Param: IntoConst<<T as AssociatedUnsigned>::Type>>(
*const T,
*const Param,
);
#[allow(dead_code)]
struct ConstLshift<T, Param: IntoConst<T>, const SHIFT: i32>(*const T, *const Param); // impl IntoConst<T>
#[allow(dead_code)]
struct ConstAdd<T, Lhs: IntoConst<T>, Rhs: IntoConst<T>>(*const T, *const Lhs, *const Rhs); // impl IntoConst<T>
/// Assigns an unsigned type to a signed type
+7
View File
@@ -0,0 +1,7 @@
mod ref_maker;
mod zerocopy_ref_ext;
mod zerocopy_slice_ext;
pub use ref_maker::*;
pub use zerocopy_ref_ext::*;
pub use zerocopy_slice_ext::*;
+107
View File
@@ -0,0 +1,107 @@
use std::marker::PhantomData;
use anyhow::{ensure, Context};
use zerocopy::{ByteSlice, ByteSliceMut, Ref};
use zeroize::Zeroize;
use crate::zeroize::ZeroizedExt;
#[derive(Clone, Copy, Debug)]
pub struct RefMaker<B: Sized, T> {
buf: B,
_phantom_t: PhantomData<T>,
}
impl<B, T> RefMaker<B, T> {
pub fn new(buf: B) -> Self {
let _phantom_t = PhantomData;
Self { buf, _phantom_t }
}
pub const fn target_size() -> usize {
std::mem::size_of::<T>()
}
pub fn into_buf(self) -> B {
self.buf
}
pub fn buf(&self) -> &B {
&self.buf
}
pub fn buf_mut(&mut self) -> &mut B {
&mut self.buf
}
}
impl<B: ByteSlice, T> RefMaker<B, T> {
pub fn parse(self) -> anyhow::Result<Ref<B, T>> {
self.ensure_fit()?;
Ref::<B, T>::new(self.buf).context("Parser error!")
}
pub fn from_prefix_with_tail(self) -> anyhow::Result<(Self, B)> {
self.ensure_fit()?;
let (head, tail) = self.buf.split_at(Self::target_size());
Ok((Self::new(head), tail))
}
pub fn split_prefix(self) -> anyhow::Result<(Self, Self)> {
self.ensure_fit()?;
let (head, tail) = self.buf.split_at(Self::target_size());
Ok((Self::new(head), Self::new(tail)))
}
pub fn from_prefix(self) -> anyhow::Result<Self> {
Ok(Self::from_prefix_with_tail(self)?.0)
}
pub fn from_suffix_with_head(self) -> anyhow::Result<(Self, B)> {
self.ensure_fit()?;
let point = self.bytes().len() - Self::target_size();
let (head, tail) = self.buf.split_at(point);
Ok((Self::new(tail), head))
}
pub fn split_suffix(self) -> anyhow::Result<(Self, Self)> {
self.ensure_fit()?;
let point = self.bytes().len() - Self::target_size();
let (head, tail) = self.buf.split_at(point);
Ok((Self::new(head), Self::new(tail)))
}
pub fn from_suffix(self) -> anyhow::Result<Self> {
Ok(Self::from_suffix_with_head(self)?.0)
}
pub fn bytes(&self) -> &[u8] {
self.buf().deref()
}
pub fn ensure_fit(&self) -> anyhow::Result<()> {
let have = self.bytes().len();
let need = Self::target_size();
ensure!(
need <= have,
"Buffer is undersized at {have} bytes (need {need} bytes)!"
);
Ok(())
}
}
impl<B: ByteSliceMut, T> RefMaker<B, T> {
pub fn make_zeroized(self) -> anyhow::Result<Ref<B, T>> {
self.zeroized().parse()
}
pub fn bytes_mut(&mut self) -> &mut [u8] {
self.buf_mut().deref_mut()
}
}
impl<B: ByteSliceMut, T> Zeroize for RefMaker<B, T> {
fn zeroize(&mut self) {
self.bytes_mut().zeroize()
}
}
+27
View File
@@ -0,0 +1,27 @@
use zerocopy::{ByteSlice, ByteSliceMut, Ref};
pub trait ZerocopyEmancipateExt<B, T> {
fn emancipate(&self) -> Ref<&[u8], T>;
}
pub trait ZerocopyEmancipateMutExt<B, T> {
fn emancipate_mut(&mut self) -> Ref<&mut [u8], T>;
}
impl<B, T> ZerocopyEmancipateExt<B, T> for Ref<B, T>
where
B: ByteSlice,
{
fn emancipate(&self) -> Ref<&[u8], T> {
Ref::new(self.bytes()).unwrap()
}
}
impl<B, T> ZerocopyEmancipateMutExt<B, T> for Ref<B, T>
where
B: ByteSliceMut,
{
fn emancipate_mut(&mut self) -> Ref<&mut [u8], T> {
Ref::new(self.bytes_mut()).unwrap()
}
}
+39
View File
@@ -0,0 +1,39 @@
use zerocopy::{ByteSlice, ByteSliceMut, Ref};
use super::RefMaker;
pub trait ZerocopySliceExt: Sized + ByteSlice {
fn zk_ref_maker<T>(self) -> RefMaker<Self, T> {
RefMaker::<Self, T>::new(self)
}
fn zk_parse<T>(self) -> anyhow::Result<Ref<Self, T>> {
self.zk_ref_maker().parse()
}
fn zk_parse_prefix<T>(self) -> anyhow::Result<Ref<Self, T>> {
self.zk_ref_maker().from_prefix()?.parse()
}
fn zk_parse_suffix<T>(self) -> anyhow::Result<Ref<Self, T>> {
self.zk_ref_maker().from_suffix()?.parse()
}
}
impl<B: ByteSlice> ZerocopySliceExt for B {}
pub trait ZerocopyMutSliceExt: ZerocopySliceExt + Sized + ByteSliceMut {
fn zk_zeroized<T>(self) -> anyhow::Result<Ref<Self, T>> {
self.zk_ref_maker().make_zeroized()
}
fn zk_zeroized_from_prefix<T>(self) -> anyhow::Result<Ref<Self, T>> {
self.zk_ref_maker().from_prefix()?.make_zeroized()
}
fn zk_zeroized_from_suffix<T>(self) -> anyhow::Result<Ref<Self, T>> {
self.zk_ref_maker().from_suffix()?.make_zeroized()
}
}
impl<B: ByteSliceMut> ZerocopyMutSliceExt for B {}
+2
View File
@@ -0,0 +1,2 @@
mod zeroized_ext;
pub use zeroized_ext::*;
+10
View File
@@ -0,0 +1,10 @@
use zeroize::Zeroize;
pub trait ZeroizedExt: Zeroize + Sized {
fn zeroized(mut self) -> Self {
self.zeroize();
self
}
}
impl<T: Zeroize + Sized> ZeroizedExt for T {}
+2 -2
View File
@@ -88,7 +88,7 @@ where
None => return Ok(None),
};
let typ = res.get(0).ok_or(invalid_msg_poller())?;
let typ = res.first().ok_or(invalid_msg_poller())?;
let typ = msgs::MsgType::try_from(*typ)?;
let msgs::MsgType::SetPsk = typ; // Assert type
@@ -113,7 +113,7 @@ where
fn set_psk(&mut self, config: SerializedBrokerConfig) -> Result<(), Self::Error> {
let config: Result<NetworkBrokerConfig, NetworkBrokerConfigErr> = config.try_into();
let config = config.map_err(|e| BrokerClientSetPskError::BrokerError(e))?;
let config = config.map_err(BrokerClientSetPskError::BrokerError)?;
use BrokerClientSetPskError::*;
const BUF_SIZE: usize = REQUEST_MSG_BUFFER_SIZE;
+6 -6
View File
@@ -11,12 +11,12 @@ pub struct NetworkBrokerConfig<'a> {
pub psk: &'a Secret<WG_KEY_LEN>,
}
impl<'a> Into<SerializedBrokerConfig<'a>> for NetworkBrokerConfig<'a> {
fn into(self) -> SerializedBrokerConfig<'a> {
SerializedBrokerConfig {
interface: self.iface.as_bytes(),
peer_id: self.peer_id,
psk: self.psk,
impl<'a> From<NetworkBrokerConfig<'a>> for SerializedBrokerConfig<'a> {
fn from(src: NetworkBrokerConfig<'a>) -> SerializedBrokerConfig<'a> {
Self {
interface: src.iface.as_bytes(),
peer_id: src.peer_id,
psk: src.psk,
additional_params: &[],
}
}
+1 -2
View File
@@ -1,4 +1,3 @@
use std::result::Result;
use std::str::{from_utf8, Utf8Error};
use zerocopy::{AsBytes, FromBytes, FromZeroes};
@@ -43,7 +42,7 @@ impl SetPskRequest {
self.iface_size = iface.len() as u8;
self.iface_buf = [0; 255];
(&mut self.iface_buf[..iface.len()]).copy_from_slice(iface);
self.iface_buf[..iface.len()].copy_from_slice(iface);
Some(())
}
+1 -2
View File
@@ -1,5 +1,4 @@
use std::borrow::BorrowMut;
use std::result::Result;
use rosenpass_secret_memory::{Public, Secret};
@@ -49,7 +48,7 @@ where
) -> Result<usize, BrokerServerError> {
use BrokerServerError::*;
let typ = req.get(0).ok_or(InvalidMessage)?;
let typ = req.first().ok_or(InvalidMessage)?;
let typ = msgs::MsgType::try_from(*typ)?;
let msgs::MsgType::SetPsk = typ; // Assert type
+1 -2
View File
@@ -9,7 +9,6 @@ fn main() {
#[cfg(target_os = "linux")]
pub mod linux {
use std::io::{stdin, stdout, Read, Write};
use std::result::Result;
use rosenpass_wireguard_broker::api::msgs;
use rosenpass_wireguard_broker::api::server::BrokerServer;
@@ -60,7 +59,7 @@ pub mod linux {
// Write the response
stdout.write_all(&(res.len() as u64).to_le_bytes())?;
stdout.write_all(&res)?;
stdout.write_all(res)?;
stdout.flush()?;
}
}
+2 -2
View File
@@ -121,7 +121,7 @@ async fn direct_broker_process(
// Read the message itself
let mut res_buf = request; // Avoid allocating memory if we don't have to
res_buf.resize(len as usize, 0);
res_buf.resize(len, 0);
stdout.read_exact(&mut res_buf[..len]).await?;
// Return to the unix socket connection worker
@@ -163,7 +163,7 @@ async fn on_accept(queue: mpsc::Sender<BrokerRequest>, mut stream: UnixStream) -
);
// Read the message itself
req_buf.resize(len as usize, 0);
req_buf.resize(len, 0);
stream.read_exact(&mut req_buf[..len]).await?;
// Handle the message
+10 -16
View File
@@ -52,23 +52,17 @@ impl MioBrokerClient {
// This sucks
match self.inner.poll_response() {
Ok(res) => {
return Ok(res);
}
Err(BrokerClientPollResponseError::IoError(e)) => {
return Err(e);
}
Err(BrokerClientPollResponseError::InvalidMessage) => {
bail!("Invalid message");
}
};
Ok(res) => Ok(res),
Err(BrokerClientPollResponseError::IoError(e)) => Err(e),
Err(BrokerClientPollResponseError::InvalidMessage) => bail!("Invalid message"),
}
}
}
impl WireGuardBroker for MioBrokerClient {
type Error = anyhow::Error;
fn set_psk<'a>(&mut self, config: SerializedBrokerConfig<'a>) -> anyhow::Result<()> {
fn set_psk(&mut self, config: SerializedBrokerConfig<'_>) -> anyhow::Result<()> {
use BrokerClientSetPskError::*;
let e = self.inner.set_psk(config);
match e {
@@ -115,7 +109,7 @@ impl BrokerClientIo for MioBrokerClientIo {
fn send_msg(&mut self, buf: &[u8]) -> Result<(), Self::SendError> {
self.flush()?;
self.send_or_buffer(&(buf.len() as u64).to_le_bytes())?;
self.send_or_buffer(&buf)?;
self.send_or_buffer(buf)?;
self.flush()?;
Ok(())
@@ -192,7 +186,7 @@ impl MioBrokerClientIo {
self.send_buf.drain(..written);
(&self.socket).try_io(|| (&self.socket).flush())?;
self.socket.try_io(|| (&self.socket).flush())?;
res
}
@@ -204,7 +198,7 @@ impl MioBrokerClientIo {
off += raw_send(&self.socket, buf)?;
}
self.send_buf.extend((&buf[off..]).iter());
self.send_buf.extend(buf[off..].iter());
Ok(())
}
@@ -231,7 +225,7 @@ fn raw_send(mut socket: &mio::net::UnixStream, data: &[u8]) -> anyhow::Result<us
}
})?;
return Ok(off);
Ok(off)
}
fn raw_recv(mut socket: &mio::net::UnixStream, out: &mut [u8]) -> anyhow::Result<usize> {
@@ -255,5 +249,5 @@ fn raw_recv(mut socket: &mio::net::UnixStream, out: &mut [u8]) -> anyhow::Result
}
})?;
return Ok(off);
Ok(off)
}
+4 -5
View File
@@ -87,21 +87,20 @@ impl WireGuardBroker for NetlinkWireGuardBroker {
.sock
.get_device(wg::DeviceInterface::from_name(config.iface))?;
if state
if !state
.peers
.iter()
.find(|p| &p.public_key == &config.peer_id.value)
.is_none()
.any(|p| p.public_key == config.peer_id.value)
{
return Err(SetPskError::NoSuchPeer);
}
// Peer update description
let mut set_peer = wireguard_uapi::set::Peer::from_public_key(&config.peer_id);
let mut set_peer = wireguard_uapi::set::Peer::from_public_key(config.peer_id);
set_peer
.flags
.push(wireguard_uapi::linux::set::WgPeerF::UpdateOnly);
set_peer.preshared_key = Some(&config.psk.secret());
set_peer.preshared_key = Some(config.psk.secret());
// Device update description
let mut set_dev = wireguard_uapi::set::Device::from_ifname(config.iface);
+1 -1
View File
@@ -1,5 +1,5 @@
use rosenpass_secret_memory::{Public, Secret};
use std::{fmt::Debug, result::Result};
use std::fmt::Debug;
pub const WG_KEY_LEN: usize = 32;
pub const WG_PEER_LEN: usize = 32;
+1
View File
@@ -36,6 +36,7 @@ mod integration_tests {
impl WireGuardBroker for MockServerBroker {
type Error = SetPskError;
#[allow(clippy::clone_on_copy)]
fn set_psk(&mut self, config: SerializedBrokerConfig) -> Result<(), Self::Error> {
loop {
let mut lock = self.inner.try_lock();