//! Wire format: the 5-byte protocol header (§6.1) and the application [`Frame`] enum (§6.3). //! //! Every Aura protocol message on the wire is a 5-byte header followed by a payload: //! //! ```text //! byte 0 : msg_type (u8) //! bytes 1..4 : length (u24, big-endian) = payload length in bytes //! byte 4 : version = 0x01 //! bytes 5.. : payload (length bytes) //! ``` //! //! [`Frame`] is the post-handshake application payload. Each `Frame` is serialized with //! [`Frame::encode`], AEAD-sealed, and shipped inside a [`MsgType::Data`] record (see //! [`crate::session`]). use bytes::Bytes; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use crate::ProtoError; /// Length in bytes of the protocol frame header. pub const HEADER_LEN: usize = 5; /// Protocol version carried in byte 4 of every header. pub const PROTOCOL_VERSION: u8 = 0x01; /// Largest payload expressible by the u24 length field. pub const MAX_PAYLOAD_LEN: usize = 0x00FF_FFFF; /// Message types carried in byte 0 of the header (§6.1). #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum MsgType { /// Handshake message 1 (C->S): hybrid public key + client nonce. ClientHello = 0x01, /// Handshake message 2 (S->C): hybrid ciphertext + server nonce. ServerHello = 0x02, /// Handshake message 4 (C->S, encrypted): client cert + signature. ClientAuth = 0x03, /// Handshake message 3 (S->C, encrypted): server cert + signature. ServerAuth = 0x04, /// Handshake Finished (encrypted): HMAC over the handshake hash. Finished = 0x05, /// Application data (encrypted): an AEAD-sealed [`Frame`]. Data = 0x06, /// Fatal alert / error notification. Alert = 0xFF, } impl MsgType { /// Map the on-wire byte to a [`MsgType`]. /// /// # Errors /// Returns [`ProtoError::UnknownMsgType`] for an unrecognized byte. pub fn from_u8(b: u8) -> Result { Ok(match b { 0x01 => Self::ClientHello, 0x02 => Self::ServerHello, 0x03 => Self::ClientAuth, 0x04 => Self::ServerAuth, 0x05 => Self::Finished, 0x06 => Self::Data, 0xFF => Self::Alert, other => return Err(ProtoError::UnknownMsgType(other)), }) } } /// Build a 5-byte header for `msg_type` carrying a payload of `payload_len` bytes. /// /// # Errors /// Returns [`ProtoError::FrameTooLarge`] if `payload_len` does not fit in the u24 length field. pub fn encode_header( msg_type: MsgType, payload_len: usize, ) -> Result<[u8; HEADER_LEN], ProtoError> { if payload_len > MAX_PAYLOAD_LEN { return Err(ProtoError::FrameTooLarge(payload_len)); } let len = payload_len as u32; Ok([ msg_type as u8, ((len >> 16) & 0xFF) as u8, ((len >> 8) & 0xFF) as u8, (len & 0xFF) as u8, PROTOCOL_VERSION, ]) } /// Parse a 5-byte header into `(msg_type, payload_len)`. /// /// # Errors /// Returns [`ProtoError::UnknownMsgType`] for an unrecognized type byte or /// [`ProtoError::BadVersion`] if byte 4 is not [`PROTOCOL_VERSION`]. pub fn decode_header(header: &[u8; HEADER_LEN]) -> Result<(MsgType, usize), ProtoError> { let msg_type = MsgType::from_u8(header[0])?; let version = header[4]; if version != PROTOCOL_VERSION { return Err(ProtoError::BadVersion(version)); } let len = ((header[1] as usize) << 16) | ((header[2] as usize) << 8) | (header[3] as usize); Ok((msg_type, len)) } /// Write one full frame (`header || payload`) to `writer`. /// /// # Errors /// Returns [`ProtoError::FrameTooLarge`] if the payload is too long, or [`ProtoError::Io`] on a /// write failure. pub async fn write_frame( writer: &mut W, msg_type: MsgType, payload: &[u8], ) -> Result<(), ProtoError> where W: AsyncWrite + Unpin, { let header = encode_header(msg_type, payload.len())?; writer.write_all(&header).await?; writer.write_all(payload).await?; writer.flush().await?; Ok(()) } /// A frame read off the wire: its type, the raw header bytes (useful as AEAD AAD and for the /// handshake transcript hash), and the payload. #[derive(Clone, Debug)] pub struct RawFrame { /// The decoded message type. pub msg_type: MsgType, /// The 5 header bytes exactly as transmitted. pub header: [u8; HEADER_LEN], /// The payload bytes. pub payload: Vec, } impl RawFrame { /// The full serialized frame (`header || payload`) exactly as it appeared on the wire. /// /// Used to feed the handshake transcript hash, which must hash the bytes as transmitted. #[must_use] pub fn wire_bytes(&self) -> Vec { let mut out = Vec::with_capacity(HEADER_LEN + self.payload.len()); out.extend_from_slice(&self.header); out.extend_from_slice(&self.payload); out } } /// Read one full frame (`header || payload`) from `reader`. /// /// # Errors /// Returns [`ProtoError::Io`] on a read failure (including a truncated frame / EOF), or a header /// decode error from [`decode_header`]. pub async fn read_frame(reader: &mut R) -> Result where R: AsyncRead + Unpin, { let mut header = [0u8; HEADER_LEN]; reader.read_exact(&mut header).await?; let (msg_type, len) = decode_header(&header)?; let mut payload = vec![0u8; len]; reader.read_exact(&mut payload).await?; Ok(RawFrame { msg_type, header, payload, }) } /// Frame type tags used in the application [`Frame`] encoding (§6.3). mod frame_tag { pub const DATA: u8 = 0x01; pub const PING: u8 = 0x02; pub const PONG: u8 = 0x03; pub const CLOSE: u8 = 0x04; } /// Kinds of in-band control message carried inside a [`CONTROL_ENVELOPE_MAGIC`]-prefixed payload. /// /// The wire byte is the discriminant. Unknown values decode as [`ControlKind::Unknown`] so peers /// running older builds gracefully ignore future kinds without dropping the connection. /// /// v2's CRL push reuses the existing post-handshake [`crate::PacketConnection::send_packet`] path /// rather than introducing a new [`Frame`] variant: a real IPv4/IPv6 packet always starts with /// `0x4X` / `0x6X`, so the 4-byte magic [`CONTROL_ENVELOPE_MAGIC`] (which starts with `0xAA`) can /// be safely multiplexed alongside ordinary packets without changing the on-wire frame schema or /// any transport-level `match Frame` that already exists. /// /// v3.1 multi-hop / onion routing adds three kinds for circuit setup: /// /// * [`ControlKind::ExtendBridge`] (`0x03`) — client → relay, asking the relay to splice this /// connection to a downstream `exit_addr`. Payload is the [`encode_extend_bridge`] binary form. /// * [`ControlKind::CircuitReady`] (`0x04`) — relay → client, the bridge is up; no payload. /// * [`ControlKind::CircuitFailed`] (`0x05`) — relay → client, the bridge could not be set up; /// payload is a UTF-8 reason string. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ControlKind { /// Server -> client: push the server's current CRL (signed payload). CrlPush, /// Client -> server: acknowledge a [`ControlKind::CrlPush`]. CrlAck, /// Client -> relay: please open a bridge to the given `exit_addr` (v3.1 multi-hop). ExtendBridge, /// Relay -> client: the bridge is up; the next bytes from the client travel opaquely to the /// exit (v3.1 multi-hop). CircuitReady, /// Relay -> client: the bridge could not be set up; payload is a UTF-8 reason string (v3.1 /// multi-hop). CircuitFailed, /// Any byte the receiver does not recognise. The connection keeps running. Unknown(u8), } impl ControlKind { /// Encode this control kind to its on-wire byte. #[must_use] pub fn to_u8(self) -> u8 { match self { ControlKind::CrlPush => 0x01, ControlKind::CrlAck => 0x02, ControlKind::ExtendBridge => 0x03, ControlKind::CircuitReady => 0x04, ControlKind::CircuitFailed => 0x05, ControlKind::Unknown(b) => b, } } /// Decode an on-wire byte into a [`ControlKind`]. Unknown bytes yield [`ControlKind::Unknown`]. #[must_use] pub fn from_u8(b: u8) -> Self { match b { 0x01 => ControlKind::CrlPush, 0x02 => ControlKind::CrlAck, 0x03 => ControlKind::ExtendBridge, 0x04 => ControlKind::CircuitReady, 0x05 => ControlKind::CircuitFailed, other => ControlKind::Unknown(other), } } } /// Encode an `ExtendBridge` payload describing the target `exit_addr`. /// /// Wire layout (big-endian where multi-byte): /// /// ```text /// family(u8 = 4|6) || addr_bytes(4 or 16) || port(u16) /// ``` /// /// The result is the **payload** of a [`ControlKind::ExtendBridge`] control envelope; the caller /// wraps it with [`encode_control_envelope`]. #[must_use] pub fn encode_extend_bridge(addr: std::net::SocketAddr) -> Vec { let port = addr.port(); match addr.ip() { std::net::IpAddr::V4(v4) => { let octets = v4.octets(); let mut out = Vec::with_capacity(1 + 4 + 2); out.push(4); out.extend_from_slice(&octets); out.extend_from_slice(&port.to_be_bytes()); out } std::net::IpAddr::V6(v6) => { let octets = v6.octets(); let mut out = Vec::with_capacity(1 + 16 + 2); out.push(6); out.extend_from_slice(&octets); out.extend_from_slice(&port.to_be_bytes()); out } } } /// Decode an `ExtendBridge` payload back into a [`std::net::SocketAddr`]. /// /// See [`encode_extend_bridge`] for the wire layout. Returns a static error string on any /// truncation, unknown family, or trailing garbage. pub fn decode_extend_bridge(payload: &[u8]) -> Result { if payload.is_empty() { return Err("ExtendBridge: empty payload"); } match payload[0] { 4 => { if payload.len() != 1 + 4 + 2 { return Err("ExtendBridge: bad v4 payload length"); } let octets: [u8; 4] = payload[1..5] .try_into() .expect("slice of length 4 converts to [u8; 4]"); let port = u16::from_be_bytes([payload[5], payload[6]]); let ip = std::net::Ipv4Addr::from(octets); Ok(std::net::SocketAddr::new(std::net::IpAddr::V4(ip), port)) } 6 => { if payload.len() != 1 + 16 + 2 { return Err("ExtendBridge: bad v6 payload length"); } let octets: [u8; 16] = payload[1..17] .try_into() .expect("slice of length 16 converts to [u8; 16]"); let port = u16::from_be_bytes([payload[17], payload[18]]); let ip = std::net::Ipv6Addr::from(octets); Ok(std::net::SocketAddr::new(std::net::IpAddr::V6(ip), port)) } _ => Err("ExtendBridge: unknown address family"), } } /// Application-level frames carried inside encrypted [`MsgType::Data`] records (§6.3). #[derive(Clone, Debug, PartialEq, Eq)] pub enum Frame { /// A stream data payload. Data { /// Logical stream identifier. stream_id: u32, /// Opaque application bytes. payload: Bytes, }, /// Liveness probe. Ping { /// Monotonic sequence number echoed back in the matching [`Frame::Pong`]. seq: u32, }, /// Reply to a [`Frame::Ping`]. Pong { /// Sequence number copied from the [`Frame::Ping`]. seq: u32, }, /// Orderly shutdown of the logical connection. Close { /// Application-defined close code. code: u8, /// Human-readable reason (UTF-8). reason: String, }, } impl Frame { /// Serialize this frame to its compact byte encoding. /// /// Layout (all multi-byte integers big-endian): /// * `Data` : `0x01 || stream_id(u32) || payload` /// * `Ping` : `0x02 || seq(u32)` /// * `Pong` : `0x03 || seq(u32)` /// * `Close` : `0x04 || code(u8) || reason_len(u32) || reason_utf8` #[must_use] pub fn encode(&self) -> Vec { let mut out = Vec::new(); match self { Frame::Data { stream_id, payload } => { out.push(frame_tag::DATA); out.extend_from_slice(&stream_id.to_be_bytes()); out.extend_from_slice(payload); } Frame::Ping { seq } => { out.push(frame_tag::PING); out.extend_from_slice(&seq.to_be_bytes()); } Frame::Pong { seq } => { out.push(frame_tag::PONG); out.extend_from_slice(&seq.to_be_bytes()); } Frame::Close { code, reason } => { out.push(frame_tag::CLOSE); out.push(*code); let bytes = reason.as_bytes(); out.extend_from_slice(&(bytes.len() as u32).to_be_bytes()); out.extend_from_slice(bytes); } } out } /// Parse a frame from its byte encoding (the inverse of [`Frame::encode`]). /// /// # Errors /// Returns [`ProtoError::MalformedFrame`] if the buffer is truncated, has an unknown tag, or /// (for `Close`) does not contain valid UTF-8. pub fn decode(buf: &[u8]) -> Result { let (&tag, rest) = buf .split_first() .ok_or(ProtoError::MalformedFrame("empty frame"))?; match tag { frame_tag::DATA => { let stream_id = read_u32(rest, "Data.stream_id")?; let payload = Bytes::copy_from_slice(&rest[4..]); Ok(Frame::Data { stream_id, payload }) } frame_tag::PING => Ok(Frame::Ping { seq: read_u32(rest, "Ping.seq")?, }), frame_tag::PONG => Ok(Frame::Pong { seq: read_u32(rest, "Pong.seq")?, }), frame_tag::CLOSE => { let code = *rest .first() .ok_or(ProtoError::MalformedFrame("Close: missing code"))?; let reason_len = read_u32(&rest[1..], "Close.reason_len")? as usize; let reason_bytes = rest .get(5..5 + reason_len) .ok_or(ProtoError::MalformedFrame("Close: truncated reason"))?; let reason = String::from_utf8(reason_bytes.to_vec()) .map_err(|_| ProtoError::MalformedFrame("Close: reason not UTF-8"))?; Ok(Frame::Close { code, reason }) } _ => Err(ProtoError::MalformedFrame("unknown frame tag")), } } } /// Read a big-endian u32 from the start of `buf`, erroring if it is too short. fn read_u32(buf: &[u8], what: &'static str) -> Result { let bytes: [u8; 4] = buf .get(..4) .ok_or(ProtoError::MalformedFrame(what))? .try_into() .expect("slice of length 4 converts to [u8; 4]"); Ok(u32::from_be_bytes(bytes)) } /// Magic prefix marking a v2 control-envelope multiplexed through [`PacketConnection::send_packet`]. /// /// An IPv4 packet's first byte is `0x4X` and an IPv6 packet's first byte is `0x6X`, so the four /// magic bytes `[0xAA, 0xAA, 0xC0, 0x01]` can never collide with a real IP packet — the TUN layer /// already rejects anything starting with a byte whose top nibble is not `4` or `6`. /// /// Envelope layout: /// /// ```text /// CONTROL_ENVELOPE_MAGIC (4 bytes) || kind (u8) || u32_be(payload_len) || payload /// ``` pub const CONTROL_ENVELOPE_MAGIC: [u8; 4] = [0xAA, 0xAA, 0xC0, 0x01]; /// Build a control envelope around `kind` + `payload`, suitable for /// [`crate::PacketConnection::send_packet`]. /// /// Layout: `MAGIC(4) || kind(u8) || u32_be(payload_len) || payload`. #[must_use] pub fn encode_control_envelope(kind: ControlKind, payload: &[u8]) -> Vec { let mut out = Vec::with_capacity(CONTROL_ENVELOPE_MAGIC.len() + 1 + 4 + payload.len()); out.extend_from_slice(&CONTROL_ENVELOPE_MAGIC); out.push(kind.to_u8()); out.extend_from_slice(&(payload.len() as u32).to_be_bytes()); out.extend_from_slice(payload); out } /// Try to decode a buffer as a control envelope. /// /// Returns `None` if `buf` does not start with [`CONTROL_ENVELOPE_MAGIC`] (i.e. it is a normal IP /// packet). Returns [`ProtoError::MalformedFrame`] if the buffer starts with the magic but is /// truncated or its length field overflows the buffer. pub fn decode_control_envelope(buf: &[u8]) -> Result)>, ProtoError> { if buf.len() < CONTROL_ENVELOPE_MAGIC.len() || &buf[..4] != CONTROL_ENVELOPE_MAGIC.as_slice() { return Ok(None); } let rest = &buf[CONTROL_ENVELOPE_MAGIC.len()..]; let kind_byte = *rest .first() .ok_or(ProtoError::MalformedFrame("control envelope: missing kind"))?; let kind = ControlKind::from_u8(kind_byte); let len_bytes: [u8; 4] = rest .get(1..5) .ok_or(ProtoError::MalformedFrame( "control envelope: missing payload length", ))? .try_into() .expect("slice of length 4 converts to [u8; 4]"); let payload_len = u32::from_be_bytes(len_bytes) as usize; let payload = rest .get(5..5 + payload_len) .ok_or(ProtoError::MalformedFrame( "control envelope: truncated payload", ))? .to_vec(); Ok(Some((kind, payload))) } #[cfg(test)] mod tests { use super::*; #[test] fn header_roundtrip_all_types() { for (ty, byte) in [ (MsgType::ClientHello, 0x01u8), (MsgType::ServerHello, 0x02), (MsgType::ClientAuth, 0x03), (MsgType::ServerAuth, 0x04), (MsgType::Finished, 0x05), (MsgType::Data, 0x06), (MsgType::Alert, 0xFF), ] { let h = encode_header(ty, 0x0012_3456).unwrap(); assert_eq!(h[0], byte); assert_eq!(h[4], PROTOCOL_VERSION); let (got_ty, got_len) = decode_header(&h).unwrap(); assert_eq!(got_ty, ty); assert_eq!(got_len, 0x0012_3456); } } #[test] fn header_rejects_oversize_and_bad_version() { assert!(matches!( encode_header(MsgType::Data, MAX_PAYLOAD_LEN + 1), Err(ProtoError::FrameTooLarge(_)) )); let mut h = encode_header(MsgType::Data, 1).unwrap(); h[4] = 0x02; assert!(matches!( decode_header(&h), Err(ProtoError::BadVersion(0x02)) )); h[0] = 0x77; assert!(matches!( decode_header(&h), Err(ProtoError::UnknownMsgType(0x77)) )); } #[test] fn frame_roundtrip() { let frames = vec![ Frame::Data { stream_id: 0xDEAD_BEEF, payload: Bytes::from_static(b"hello world"), }, Frame::Data { stream_id: 0, payload: Bytes::new(), }, Frame::Ping { seq: 42 }, Frame::Pong { seq: 0xFFFF_FFFF }, Frame::Close { code: 7, reason: "going away \u{1f44b}".to_string(), }, Frame::Close { code: 0, reason: String::new(), }, ]; for f in frames { let encoded = f.encode(); let decoded = Frame::decode(&encoded).unwrap(); assert_eq!(f, decoded); } } #[test] fn frame_decode_rejects_garbage() { assert!(Frame::decode(&[]).is_err()); assert!(Frame::decode(&[0x99]).is_err()); assert!(Frame::decode(&[frame_tag::PING, 0x00]).is_err()); // truncated u32 assert!(Frame::decode(&[frame_tag::CLOSE]).is_err()); // missing code } #[test] fn control_envelope_roundtrip() { let env = encode_control_envelope(ControlKind::CrlPush, b"hello"); assert_eq!(&env[..4], &CONTROL_ENVELOPE_MAGIC); let (kind, payload) = decode_control_envelope(&env).unwrap().unwrap(); assert_eq!(kind, ControlKind::CrlPush); assert_eq!(payload, b"hello"); } #[test] fn control_envelope_skips_normal_ip_packets() { // IPv4 packet: first byte's top nibble is 4. Never collides with magic. let ipv4 = vec![0x45u8, 0x00, 0x00, 0x14]; assert!(decode_control_envelope(&ipv4).unwrap().is_none()); // IPv6 packet: first byte's top nibble is 6. let ipv6 = vec![0x60u8, 0x00, 0x00, 0x00]; assert!(decode_control_envelope(&ipv6).unwrap().is_none()); // Random short bytes that do not match the magic. let other = vec![0xAAu8, 0xAA, 0xC0, 0x02]; assert!(decode_control_envelope(&other).unwrap().is_none()); // Shorter than the magic. assert!(decode_control_envelope(&[0xAA, 0xAA]).unwrap().is_none()); } #[test] fn control_envelope_rejects_truncated_payload() { let mut env = encode_control_envelope(ControlKind::CrlPush, b"payload-bytes"); // Trim a few bytes from the end to truncate the payload claimed by the length field. env.truncate(env.len() - 3); assert!(decode_control_envelope(&env).is_err()); } #[test] fn control_envelope_unknown_kind_decodes_as_unknown() { // Hand-craft an envelope with a future kind byte. let mut env = Vec::new(); env.extend_from_slice(&CONTROL_ENVELOPE_MAGIC); env.push(0x77); // unknown kind env.extend_from_slice(&3u32.to_be_bytes()); env.extend_from_slice(b"abc"); let (kind, payload) = decode_control_envelope(&env).unwrap().unwrap(); assert_eq!(kind, ControlKind::Unknown(0x77)); assert_eq!(payload, b"abc"); } /// v3.1 multi-hop: round-trip `ExtendBridge` payload over IPv4 + IPv6 addresses, including /// boundary ports. #[test] fn extend_bridge_roundtrip_v4_and_v6() { let cases: &[std::net::SocketAddr] = &[ "203.0.113.10:443".parse().unwrap(), "127.0.0.1:0".parse().unwrap(), "255.255.255.255:65535".parse().unwrap(), "[::1]:443".parse().unwrap(), "[2001:db8::1]:65000".parse().unwrap(), "[::]:0".parse().unwrap(), ]; for addr in cases { let payload = encode_extend_bridge(*addr); let decoded = decode_extend_bridge(&payload).unwrap(); assert_eq!(*addr, decoded, "addr {addr} round-tripped"); } } /// Hand-check the on-wire layout for an IPv4 case: `0x04 || octets(4) || port_be(2)`. #[test] fn extend_bridge_v4_wire_layout() { let addr: std::net::SocketAddr = "10.0.0.42:443".parse().unwrap(); let p = encode_extend_bridge(addr); assert_eq!(p.len(), 1 + 4 + 2); assert_eq!(p[0], 4); assert_eq!(&p[1..5], &[10, 0, 0, 42]); assert_eq!(&p[5..7], &443u16.to_be_bytes()); } /// Hand-check the on-wire layout for an IPv6 case: `0x06 || octets(16) || port_be(2)`. #[test] fn extend_bridge_v6_wire_layout() { let addr: std::net::SocketAddr = "[2001:db8::1]:443".parse().unwrap(); let p = encode_extend_bridge(addr); assert_eq!(p.len(), 1 + 16 + 2); assert_eq!(p[0], 6); assert_eq!(&p[17..19], &443u16.to_be_bytes()); } /// Malformed `ExtendBridge` payloads are rejected (empty / wrong family / bad length). #[test] fn extend_bridge_rejects_bad_inputs() { assert!(decode_extend_bridge(&[]).is_err()); // Unknown family. assert!(decode_extend_bridge(&[7u8, 0, 0, 0, 0, 0, 0]).is_err()); // v4 family but truncated. assert!(decode_extend_bridge(&[4u8, 1, 2, 3]).is_err()); // v4 family but extra trailing byte (should be exactly 7 bytes). assert!(decode_extend_bridge(&[4u8, 1, 2, 3, 4, 0, 0, 0]).is_err()); // v6 family but truncated. let mut bad6 = vec![6u8]; bad6.extend_from_slice(&[0u8; 10]); assert!(decode_extend_bridge(&bad6).is_err()); } /// `ControlKind` byte mapping is stable for every v3.1 variant. #[test] fn control_kind_bytes_stable() { assert_eq!(ControlKind::ExtendBridge.to_u8(), 0x03); assert_eq!(ControlKind::CircuitReady.to_u8(), 0x04); assert_eq!(ControlKind::CircuitFailed.to_u8(), 0x05); assert_eq!(ControlKind::from_u8(0x03), ControlKind::ExtendBridge); assert_eq!(ControlKind::from_u8(0x04), ControlKind::CircuitReady); assert_eq!(ControlKind::from_u8(0x05), ControlKind::CircuitFailed); } /// A `CircuitFailed` envelope round-trips with a UTF-8 reason string. #[test] fn circuit_failed_envelope_roundtrip() { let reason = "not in allow_extend_to"; let env = encode_control_envelope(ControlKind::CircuitFailed, reason.as_bytes()); let (kind, payload) = decode_control_envelope(&env).unwrap().unwrap(); assert_eq!(kind, ControlKind::CircuitFailed); assert_eq!(std::str::from_utf8(&payload).unwrap(), reason); } }