use buffa::Message;
#[rustfmt::skip]
#[allow(missing_docs, clippy::derivable_impls)]
mod generated;
use crate::ServiceIdentity;
pub use generated::{Control, Health, Hello, MessageKind};
use std::io::{self, Read, Write};
pub const MAGIC: [u8; 8] = *b"KNDPB002";
pub const VERSION: u32 = 2;
pub const MAX_FRAME: u32 = 65_536;
pub const HELLO_LIMIT: u32 = 1_024;
pub const MIN_FRAME: u32 = 256;
pub mod capability {
pub const HEALTH: u64 = 1;
pub const DRAIN: u64 = 2;
pub const HANDOFF: u64 = 4;
pub const APPLICATION: u64 = 8;
pub const HEALTH_DETAILS: u64 = 16;
}
impl Health {
pub fn response(&self, request: &Control) -> io::Result<Control> {
if request.kind != MessageKind::Lifecycle
|| !matches!(request.operation, operation::HEALTH | operation::DRAIN)
{
return Err(invalid("not a health or drain request"));
}
Ok(Control {
operation: request.operation,
request_id: request.request_id,
generation: self.generation,
payload: self.encode_to_vec(),
..Default::default()
})
}
pub fn from_response(reply: &Control, request: &Control) -> io::Result<Self> {
if reply.kind != MessageKind::Lifecycle
|| !matches!(request.operation, operation::HEALTH | operation::DRAIN)
|| reply.operation != request.operation
|| reply.request_id != request.request_id
|| !reply.token.is_empty()
|| reply.offset.is_some()
{
return Err(invalid("unexpected health response"));
}
let health: Self = decode_message(&reply.payload, MAX_FRAME)?;
if health.process_id == 0
|| health.generation != reply.generation
|| (health.ready && health.draining)
{
return Err(invalid("invalid health observation"));
}
Ok(health)
}
}
pub mod operation {
pub const HEALTH: u32 = 1;
pub const DRAIN: u32 = 2;
pub const PREPARE: u32 = 3;
pub const PAUSED: u32 = 4;
pub const READY: u32 = 5;
pub const COMMIT: u32 = 6;
pub const ABORT: u32 = 7;
}
impl Hello {
pub fn new(identity: &ServiceIdentity, supported: u64, required: u64) -> Self {
Self {
minimum: VERSION,
maximum: VERSION,
supported,
required,
max_frame: MAX_FRAME,
application: identity.application().into(),
profile: identity.profile().into(),
instance: identity.instance().into(),
service_id: identity.id().to_vec(),
..Default::default()
}
}
fn validate(&self) -> io::Result<()> {
ServiceIdentity::new(
self.service_id
.as_slice()
.try_into()
.map_err(|_| invalid("service UUID must have 16 bytes"))?,
&self.application,
&self.profile,
&self.instance,
)
.map_err(|_| invalid("invalid service identity"))?;
if !Terms::of(self).valid() {
return Err(invalid("invalid version or capability offer"));
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(kani, derive(kani::Arbitrary))]
struct Terms {
minimum: u32,
maximum: u32,
supported: u64,
required: u64,
max_frame: u32,
}
impl Terms {
fn of(hello: &Hello) -> Self {
Self {
minimum: hello.minimum,
maximum: hello.maximum,
supported: hello.supported,
required: hello.required,
max_frame: hello.max_frame,
}
}
fn valid(self) -> bool {
self.minimum != 0
&& self.minimum <= self.maximum
&& self.required & !self.supported == 0
&& (MIN_FRAME..=MAX_FRAME).contains(&self.max_frame)
}
fn agree(self, remote: Self) -> io::Result<Agreement> {
let minimum = self.minimum.max(remote.minimum);
let maximum = self.maximum.min(remote.maximum);
if minimum > VERSION || maximum < VERSION {
return Err(invalid("no implemented binary version in common"));
}
let capabilities = self.supported & remote.supported;
if (self.required | remote.required) & !capabilities != 0 {
return Err(invalid("required capability unavailable"));
}
Ok(Agreement {
version: VERSION,
capabilities,
max_frame: self.max_frame.min(remote.max_frame),
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Agreement {
pub version: u32,
pub capabilities: u64,
pub max_frame: u32,
}
pub fn negotiate(local: &Hello, remote: &Hello) -> io::Result<Agreement> {
local.validate()?;
remote.validate()?;
if local.service_id != remote.service_id
|| local.application != remote.application
|| local.profile != remote.profile
|| local.instance != remote.instance
{
return Err(invalid("service identity mismatch"));
}
Terms::of(local).agree(Terms::of(remote))
}
fn required_capability(message: &Control) -> io::Result<u64> {
match message.kind.as_known() {
Some(MessageKind::Application) if message.operation != 0 => {
return Ok(capability::APPLICATION);
}
Some(MessageKind::Lifecycle) => {}
_ => return Err(invalid("invalid message kind or application operation")),
}
use self::{capability as c, operation as o};
match message.operation {
o::HEALTH => Ok(c::HEALTH),
o::DRAIN => Ok(c::DRAIN),
o::PREPARE..=o::ABORT => Ok(c::HANDOFF),
_ => Err(invalid("unknown control operation")),
}
}
fn invalid(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidData, message)
}
fn encode_frame<M: Message>(
message: &M,
limit: u32,
buffer: &mut Vec<u8>,
cache: &mut buffa::SizeCache,
) -> io::Result<()> {
buffer.clear();
buffer.extend_from_slice(&[0; 4]);
let length = message
.try_encode_bounded_with_cache(limit, cache, buffer)
.map_err(io::Error::other)?;
if length == 0 {
return Err(invalid("empty message"));
}
buffer[..4].copy_from_slice(&length.to_le_bytes());
Ok(())
}
fn decode_message<M: Message>(buffer: &[u8], limit: u32) -> io::Result<M> {
buffa::DecodeOptions::new()
.with_max_message_size(limit as usize)
.with_recursion_limit(32)
.with_unknown_field_limit(128)
.with_element_memory_limit(MAX_FRAME as usize)
.decode_from_slice(buffer)
.map_err(io::Error::other)
}
pub struct Framed<S> {
io: S,
buffer: Vec<u8>,
cache: buffa::SizeCache,
limit: u32,
failed: bool,
}
impl<S: Read + Write> Framed<S> {
pub fn new(io: S, limit: u32) -> io::Result<Self> {
if !(MIN_FRAME..=MAX_FRAME).contains(&limit) {
return Err(invalid("invalid frame limit"));
}
Ok(Self {
io,
buffer: Vec::new(),
cache: buffa::SizeCache::new(),
limit,
failed: false,
})
}
fn usable(&self) -> io::Result<()> {
if self.failed {
Err(invalid("session failed; reconnect without replay"))
} else {
Ok(())
}
}
pub fn send<M: Message>(&mut self, message: &M) -> io::Result<()> {
self.usable()?;
encode_frame(message, self.limit, &mut self.buffer, &mut self.cache)?;
self.failed = true;
self.io.write_all(&self.buffer)?;
self.io.flush()?;
self.failed = false;
Ok(())
}
pub fn receive<M: Message + Default>(&mut self) -> io::Result<M> {
self.usable()?;
self.failed = true;
let mut length = [0; 4];
self.io.read_exact(&mut length)?;
let length = u32::from_le_bytes(length);
if length == 0 || length > self.limit {
return Err(invalid("received frame exceeds negotiated bounds"));
}
self.buffer.resize(length as usize, 0);
self.io.read_exact(&mut self.buffer)?;
let value = decode_message(self.buffer.as_slice(), self.limit)?;
self.failed = false;
Ok(value)
}
}
pub struct Session<S> {
frames: Framed<S>,
agreement: Agreement,
}
impl<S: Read + Write> Session<S> {
pub fn connect(mut io: S, offer: &Hello) -> io::Result<Self> {
offer.validate()?;
io.write_all(&MAGIC)?;
let mut frames = Framed::new(io, HELLO_LIMIT)?;
frames.send(offer)?;
let remote: Hello = frames.receive()?;
let agreement = negotiate(offer, &remote)?;
frames.io.write_all(&[1])?;
frames.io.flush()?;
read_acceptance(&mut frames.io)?;
frames.limit = agreement.max_frame;
Ok(Self { frames, agreement })
}
pub fn accept(mut io: S, offer: &Hello) -> io::Result<Self> {
offer.validate()?;
let mut magic = [0; 8];
io.read_exact(&mut magic)?;
if magic != MAGIC {
return Err(invalid("wrong binary protocol magic"));
}
let mut frames = Framed::new(io, HELLO_LIMIT)?;
let remote: Hello = frames.receive()?;
let agreement = negotiate(offer, &remote)?;
frames.send(offer)?;
read_acceptance(&mut frames.io)?;
frames.io.write_all(&[1])?;
frames.io.flush()?;
frames.limit = agreement.max_frame;
Ok(Self { frames, agreement })
}
pub fn transport_mut(&mut self) -> &mut S {
&mut self.frames.io
}
pub fn agreement(&self) -> Agreement {
self.agreement
}
pub fn send(&mut self, message: &Control) -> io::Result<()> {
self.check(message)?;
self.frames.send(message)
}
pub fn receive(&mut self) -> io::Result<Control> {
let message = self.frames.receive()?;
if let Err(error) = self.check(&message) {
self.frames.failed = true;
return Err(error);
}
Ok(message)
}
fn check(&self, message: &Control) -> io::Result<()> {
if required_capability(message)? & self.agreement.capabilities == 0 {
return Err(invalid("operation capability was not negotiated"));
}
Ok(())
}
}
fn read_acceptance(io: &mut impl Read) -> io::Result<()> {
let mut accepted = [0];
io.read_exact(&mut accepted)?;
if accepted != [1] {
return Err(invalid("peer did not accept negotiation"));
}
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
pub enum Endpoint<'a> {
Legacy(&'a str),
Binary(&'a str),
}
pub fn select_endpoint<'a>(legacy: &'a str, binary: Option<&'a str>) -> io::Result<Endpoint<'a>> {
if legacy.is_empty() {
return Err(invalid("missing legacy endpoint"));
}
match binary {
None => Ok(Endpoint::Legacy(legacy)),
Some(endpoint) if !endpoint.is_empty() && endpoint != legacy => {
Ok(Endpoint::Binary(endpoint))
}
Some(_) => Err(invalid("binary endpoint must be distinct and nonempty")),
}
}
#[cfg(feature = "wire-async")]
mod asynchronous;
#[cfg(feature = "wire-async")]
pub use asynchronous::AsyncSession;
#[cfg(kani)]
mod proofs {
use super::*;
fn valid_offer() -> Terms {
let terms: Terms = kani::any();
kani::assume(terms.valid());
terms
}
#[kani::proof]
fn both_peers_reach_the_same_agreement() {
let a = valid_offer();
let b = valid_offer();
match (a.agree(b), b.agree(a)) {
(Ok(x), Ok(y)) => assert!(x == y),
(Err(_), Err(_)) => {}
_ => panic!("one peer agreed and the other refused"),
}
}
#[kani::proof]
fn an_agreement_stays_within_both_offers() {
let a = valid_offer();
let b = valid_offer();
if let Ok(agreed) = a.agree(b) {
assert!((MIN_FRAME..=MAX_FRAME).contains(&agreed.max_frame));
assert!(agreed.max_frame <= a.max_frame && agreed.max_frame <= b.max_frame);
assert!(agreed.capabilities & !a.supported == 0);
assert!(agreed.capabilities & !b.supported == 0);
assert!((a.required | b.required) & !agreed.capabilities == 0);
assert!(a.minimum <= agreed.version && agreed.version <= a.maximum);
assert!(b.minimum <= agreed.version && agreed.version <= b.maximum);
}
}
}