Files
tsunagi/src/proto/message.rs
T

185 lines
6.2 KiB
Rust
Raw Normal View History

//! Control message formats.
//!
//! This is deliberately a small, closed set of messages, not a general RPC
//! framework. Bodies are encoded with [postcard], a compact, deterministic,
//! non-self-describing serde format.
//!
//! Every message that arrives from the network passes [`validate`] against the
//! configured [`Limits`] before it reaches anything else.
//!
//! [postcard]: https://docs.rs/postcard
use serde::{Deserialize, Serialize};
use crate::config::Limits;
use crate::dataplane::{MAX_PROTOCOL_ID_LEN, PluginCapability};
use crate::error::ProtocolError;
/// ALPN of the tsunagi control plane.
///
/// The version in the ALPN is the wire-compatibility version of the control
/// protocol. It is independent of the network identity scheme version, so
/// bumping it must not change any existing [`crate::NetworkId`].
pub const ALPN: &[u8] = b"tsunagi/ctrl/1";
/// Control protocol version carried inside the handshake.
pub const PROTOCOL_VERSION: u16 = 1;
/// First message of the handshake, sent by the initiator.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Hello {
/// Control protocol version the initiator speaks.
pub version: u16,
/// Public network identifier the initiator wants to join.
pub network_id: [u8; 32],
/// Initiator's fresh handshake nonce.
pub nonce: [u8; 16],
}
/// Responder's reply to [`Hello`]. Carries no proof yet.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HelloAck {
/// Control protocol version the responder speaks.
pub version: u16,
/// Responder's fresh handshake nonce.
pub nonce: [u8; 16],
}
/// A handshake proof, i.e. one HMAC tag over a role-specific transcript.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthProof {
/// HMAC-SHA256 tag.
pub proof: [u8; 32],
}
/// What this agent tells a peer about itself.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Announcement {
/// Human-readable hostname. A mutable binding, not an identity.
pub hostname: String,
/// Announced IP plugin capabilities. Opaque to the core.
pub capabilities: Vec<PluginCapability>,
}
/// A control message exchanged after a successful handshake.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ControlMessage {
/// Hostname and capability announcement.
Announce(Announcement),
/// A small request used to verify that the exchange works.
Ping {
/// Caller-chosen sequence number, echoed back.
seq: u64,
/// Opaque bounded payload, echoed back.
payload: Vec<u8>,
},
/// The reply to a [`ControlMessage::Ping`].
Pong {
/// Sequence number of the request being answered.
seq: u64,
/// Echoed payload.
payload: Vec<u8>,
},
/// Graceful goodbye.
///
/// A peer going away is not a revocation of anything.
Bye {
/// Short free-text reason.
reason: String,
},
}
/// A control message together with the network it belongs to.
///
/// Every session is bound to exactly one network at handshake time. The
/// `network_id` here is re-checked on every message, so an authenticated
/// session for network A can never be used to speak to network B, even over a
/// shared physical connection.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Envelope {
/// Network this message belongs to.
pub network_id: [u8; 32],
/// The message itself.
pub message: ControlMessage,
}
/// Encodes a value into a postcard byte vector.
pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, ProtocolError> {
postcard::to_stdvec(value).map_err(|_| ProtocolError::Malformed("cannot encode message"))
}
/// Decodes a value from postcard bytes.
pub fn decode<T: for<'de> Deserialize<'de>>(bytes: &[u8]) -> Result<T, ProtocolError> {
postcard::from_bytes(bytes).map_err(|_| ProtocolError::Malformed("cannot decode message"))
}
fn check_len(field: &'static str, len: usize, limit: usize) -> Result<(), ProtocolError> {
if len > limit {
return Err(ProtocolError::FieldTooLarge { field, len, limit });
}
Ok(())
}
/// Validates a decoded capability against the configured limits.
pub fn validate_capability(
capability: &PluginCapability,
limits: &Limits,
) -> Result<(), ProtocolError> {
if capability.protocol.is_empty() {
return Err(ProtocolError::Malformed("empty plugin protocol id"));
}
check_len(
"capability.protocol",
capability.protocol.len(),
MAX_PROTOCOL_ID_LEN,
)?;
check_len(
"capability.data",
capability.data.len(),
limits.max_capability_data_len,
)?;
Ok(())
}
/// Validates a decoded control message against the configured limits.
///
/// Returning an error rejects that single message. It never stops the session's
/// network, the other networks or the agent.
pub fn validate(message: &ControlMessage, limits: &Limits) -> Result<(), ProtocolError> {
match message {
ControlMessage::Announce(announcement) => {
check_len(
"announce.hostname",
announcement.hostname.len(),
limits.max_hostname_len,
)?;
check_len(
"announce.capabilities",
announcement.capabilities.len(),
limits.max_capabilities,
)?;
for capability in &announcement.capabilities {
validate_capability(capability, limits)?;
}
}
ControlMessage::Ping { payload, .. } | ControlMessage::Pong { payload, .. } => {
check_len("echo.payload", payload.len(), limits.max_echo_payload_len)?;
}
ControlMessage::Bye { reason } => {
check_len("bye.reason", reason.len(), limits.max_reason_len)?;
}
}
Ok(())
}
/// A short, stable label for a message kind, for metrics and diagnostics.
pub fn kind(message: &ControlMessage) -> &'static str {
match message {
ControlMessage::Announce(_) => "announce",
ControlMessage::Ping { .. } => "ping",
ControlMessage::Pong { .. } => "pong",
ControlMessage::Bye { .. } => "bye",
}
}