//! 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, } /// 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, }, /// The reply to a [`ControlMessage::Ping`]. Pong { /// Sequence number of the request being answered. seq: u64, /// Echoed payload. payload: Vec, }, /// 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(value: &T) -> Result, ProtocolError> { postcard::to_stdvec(value).map_err(|_| ProtocolError::Malformed("cannot encode message")) } /// Decodes a value from postcard bytes. pub fn decode Deserialize<'de>>(bytes: &[u8]) -> Result { 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", } }