diff --git a/crates/scion-proto/src/lib.rs b/crates/scion-proto/src/lib.rs index 69685b2..38b9bea 100644 --- a/crates/scion-proto/src/lib.rs +++ b/crates/scion-proto/src/lib.rs @@ -5,6 +5,7 @@ pub mod datagram; pub mod packet; pub mod path; pub mod reliable; +pub mod scmp; pub(crate) mod utils; pub mod wire_encoding; diff --git a/crates/scion-proto/src/scmp.rs b/crates/scion-proto/src/scmp.rs new file mode 100644 index 0000000..18eb164 --- /dev/null +++ b/crates/scion-proto/src/scmp.rs @@ -0,0 +1,15 @@ +//! Types and conversion for the SCION Control Message Protocol. +//! +//! This implements the specification at the [SCION documentation page][scion-doc-scmp] but currently +//! does not cover DRKey-based authentication. +//! +//! [scion-doc-scmp]: https://docs.scion.org/en/latest/protocols/scmp.html + +pub mod error; +pub use error::ScmpDecodeError; + +pub mod messages; +pub use messages::{ScmpMessage, ScmpType}; + +pub mod raw; +pub use raw::ScmpMessageRaw; diff --git a/crates/scion-proto/src/scmp/error.rs b/crates/scion-proto/src/scmp/error.rs new file mode 100644 index 0000000..ff64ff2 --- /dev/null +++ b/crates/scion-proto/src/scmp/error.rs @@ -0,0 +1,24 @@ +//! Errors encountered when handling SCMP messages. + +/// Error encountered when attempting to decode an SCMP message. +#[derive(Debug, thiserror::Error)] +pub enum ScmpDecodeError { + /// The data is shorter than the minimum length of the corresponding SCMP message. + #[error("message is empty or was truncated")] + MessageEmptyOrTruncated, + /// When attempting to decode a specific message type and the data contains a different message + /// type. + #[error("the type of the message does not match the type being decoded")] + MessageTypeMismatch, + /// Informational messages of unknown types need to be dropped. + #[error("unknown info message type {0}")] + UnknownInfoMessage(u8), + /// Depending on the type of SCMP message, only specific values of the `code` field are allowed. + #[error("invalid code for this message type")] + InvalidCode, + /// When decoding a SCION packet presumably containing an SCMP message but the next-header value + /// of the SCION header doesn't match + /// [`ScmpMessageRaw::PROTOCOL_NUMBER`][super::ScmpMessageRaw::PROTOCOL_NUMBER]. + #[error("next-header value of SCION header is not correct")] + WrongProtocolNumber(u8), +} diff --git a/crates/scion-proto/src/scmp/messages.rs b/crates/scion-proto/src/scmp/messages.rs new file mode 100644 index 0000000..2639246 --- /dev/null +++ b/crates/scion-proto/src/scmp/messages.rs @@ -0,0 +1,718 @@ +//! Specific individual SCMP messages and their types. + +use bytes::{Buf, BufMut, Bytes}; + +use super::{ScmpDecodeError, ScmpMessageRaw}; +use crate::{ + address::IsdAsn, + packet::{ChecksumDigest, InadequateBufferSize}, + utils::encoded_type, + wire_encoding::WireEncodeVec, +}; + +/// Fully decoded SCMP message with an appropriate format. +/// +/// The different variants correspond to the [`ScmpType`] variants. +#[allow(missing_docs)] +#[derive(Debug, PartialEq, Clone)] +pub enum ScmpMessage { + DestinationUnreachable(ScmpDestinationUnreachable), + PacketTooBig(ScmpPacketTooBig), + ParameterProblem(ScmpParameterProblem), + ExternalInterfaceDown(ScmpExternalInterfaceDown), + InternalConnectivityDown(ScmpInternalConnectivityDown), + EchoRequest(ScmpEchoRequest), + EchoReply(ScmpEchoReply), + TracerouteRequest(ScmpTracerouteRequest), + TracerouteReply(ScmpTracerouteReply), + // This is needed because the specification states: + // "If an SCMP error message of unknown type is received at its destination, it MUST be passed + // to the upper-layer process that originated the packet that caused the error, if it can be + // identified." + UnknownError(ScmpMessageRaw), + // There is no `UnknownInformational` variant because the specification states: + // "If an SCMP informational message of unknown type is received, it MUST be silently dropped." +} + +macro_rules! lift_fn_from_scmp_variants { + ( + $(#[$outer:meta])* + $vis:vis fn $name:ident(&self $(,$param:ident : $param_type:ty)*) -> $return_type:ty + ) => { + $(#[$outer])* + $vis fn $name(&self $(,$param : $param_type)*) -> $return_type { + match self { + Self::DestinationUnreachable(x) => x.$name($($param),*), + Self::PacketTooBig(x) => x.$name($($param),*), + Self::ParameterProblem(x) => x.$name($($param),*), + Self::ExternalInterfaceDown(x) => x.$name($($param),*), + Self::InternalConnectivityDown(x) => x.$name($($param),*), + Self::EchoRequest(x) => x.$name($($param),*), + Self::EchoReply(x) => x.$name($($param),*), + Self::TracerouteRequest(x) => x.$name($($param),*), + Self::TracerouteReply(x) => x.$name($($param),*), + Self::UnknownError(x) => x.$name($($param),*), + } + } + }; +} + +impl ScmpMessage { + lift_fn_from_scmp_variants!( + /// Returns the type of the corresponding message. + pub fn get_type(&self) -> ScmpType + ); + + /// Returns true iff [`self`] is an error message. + pub fn is_error(&self) -> bool { + self.get_type().is_error() + } + + /// Returns true iff [`self`] is an informational message. + pub fn is_informational(&self) -> bool { + self.get_type().is_informational() + } + + /// Returns true for all supported SCMP messages and false otherwise. + pub fn is_supported(&self) -> bool { + !matches!(self, Self::UnknownError(_)) + } +} + +impl TryFrom for ScmpMessage { + type Error = ScmpDecodeError; + + fn try_from(value: ScmpMessageRaw) -> Result { + Ok(match value.message_type { + ScmpType::DestinationUnreachable => { + Self::DestinationUnreachable(ScmpDestinationUnreachable::try_from(value)?) + } + ScmpType::PacketTooBig => Self::PacketTooBig(ScmpPacketTooBig::try_from(value)?), + ScmpType::ParameterProblem => { + Self::ParameterProblem(ScmpParameterProblem::try_from(value)?) + } + ScmpType::ExternalInterfaceDown => { + Self::ExternalInterfaceDown(ScmpExternalInterfaceDown::try_from(value)?) + } + ScmpType::InternalConnectivityDown => { + Self::InternalConnectivityDown(ScmpInternalConnectivityDown::try_from(value)?) + } + ScmpType::EchoRequest => Self::EchoRequest(ScmpEchoRequest::try_from(value)?), + ScmpType::EchoReply => Self::EchoReply(ScmpEchoReply::try_from(value)?), + ScmpType::TracerouteRequest => { + Self::TracerouteRequest(ScmpTracerouteRequest::try_from(value)?) + } + ScmpType::TracerouteReply => { + Self::TracerouteReply(ScmpTracerouteReply::try_from(value)?) + } + ScmpType::OtherError(_) => Self::UnknownError(value), + ScmpType::OtherInfo(t) => return Err(ScmpDecodeError::UnknownInfoMessage(t)), + }) + } +} + +impl WireEncodeVec<2> for ScmpMessage { + type Error = InadequateBufferSize; + + lift_fn_from_scmp_variants!( + fn encode_with_unchecked(&self, buffer: &mut bytes::BytesMut) -> [Bytes; 2] + ); + + lift_fn_from_scmp_variants!(fn total_length(&self) -> usize); + + lift_fn_from_scmp_variants!(fn required_capacity(&self) -> usize); +} + +trait ScmpMessageEncodeDecode: Sized { + const INFO_BLOCK_LENGTH: usize; + + #[allow(unused_variables)] + fn encode_info_block_unchecked(&self, buffer: &mut impl BufMut) {} + + fn info_block_checksum<'a>( + &self, + base_digest: &'a mut ChecksumDigest, + ) -> &'a mut ChecksumDigest { + base_digest + } + + fn data_block(&self) -> Bytes { + Bytes::new() + } + + fn code(&self) -> u8 { + 0 + } + + fn check_code(code: u8) -> Result<(), ScmpDecodeError> { + if code != 0 { + return Err(ScmpDecodeError::InvalidCode); + } + Ok(()) + } + + /// This assumes that the function argument has the correct type and checksum and a valid code + /// for this message type. + fn from_raw_unchecked(value: ScmpMessageRaw) -> Self; +} + +impl WireEncodeVec<2> for T { + type Error = InadequateBufferSize; + + fn encode_with_unchecked(&self, buffer: &mut bytes::BytesMut) -> [Bytes; 2] { + self.encode_info_block_unchecked(buffer); + [buffer.split().freeze(), self.data_block()] + } + + #[inline] + fn total_length(&self) -> usize { + self.required_capacity() + self.data_block().len() + } + + #[inline] + fn required_capacity(&self) -> usize { + ScmpMessageRaw::FIELD_LENGTH + Self::INFO_BLOCK_LENGTH + } +} + +/// Trait implemented by all SCMP error messages. +pub trait ScmpErrorMessage { + /// Get the (truncated) packet that triggered the error. + fn get_offending_packet(&self) -> Bytes; +} + +macro_rules! impl_conversion_and_type { + ( + $name:ident : $message_type:ident + ) => { + impl From<$name> for ScmpMessage { + fn from(value: $name) -> Self { + ScmpMessage::$message_type(value) + } + } + + impl $name { + /// The SCMP type of this message type. + const MESSAGE_TYPE: ScmpType = ScmpType::$message_type; + + /// Returns the SCMP type of this message. + #[inline] + pub fn get_type(&self) -> ScmpType { + Self::MESSAGE_TYPE + } + + fn try_from(value: ScmpMessageRaw) -> Result { + if value.message_type != Self::MESSAGE_TYPE { + return Err(ScmpDecodeError::MessageTypeMismatch); + } + Self::check_code(value.code)?; + if value.payload.len() < Self::INFO_BLOCK_LENGTH { + return Err(ScmpDecodeError::MessageEmptyOrTruncated); + } + // Question(mlegner): Check upper bound of payload? + Ok(Self::from_raw_unchecked(value)) + } + } + }; +} + +macro_rules! error_message { + ( + $(#[$outer:meta])* + pub struct $name:ident : $message_type:ident {$($(#[$doc:meta])* $vis:vis $field:ident : $type:ty,)*} + ) => { + $(#[$outer])* + #[derive(Debug, Clone, PartialEq)] + pub struct $name { + $($(#[$doc])* $vis $field: $type,)* + offending_packet: Bytes, + } + + impl ScmpErrorMessage for $name { + #[inline] + fn get_offending_packet(&self) -> Bytes { + self.offending_packet.clone() + } + } + + impl_conversion_and_type!($name: $message_type); + + // Question(mlegner): Should we check the length of the offending packet? According to the + // specification, an error message should contain as much of the offending packet as + // possible without the SCMP packet (including the SCION header and all extension headers) + // exceeding 1232 bytes. + }; +} + +encoded_type!( + #[allow(missing_docs)] + pub enum DestinationUnreachableCode(u8) { + NoRouteToDestination = 0, + CommunicationAdministrativelyDenied = 1, + BeyondScopeOfSourceAddress = 2, + AddressUnreachable = 3, + PortUnreachable = 4, + SourceAddressFailedIngressEgressPolicy = 5, + RejectRouteToDestination = 6; + Unassigned = 7..=u8::MAX, + } +); +error_message!( + /// Error generated by the destination AS in response to a packet that cannot be delivered to + /// its destination address for reasons other than congestion. + pub struct ScmpDestinationUnreachable: DestinationUnreachable { + /// Encodes the reason why the destination is unreachable. + pub code: DestinationUnreachableCode, + } +); +impl ScmpMessageEncodeDecode for ScmpDestinationUnreachable { + const INFO_BLOCK_LENGTH: usize = 4; + + fn encode_info_block_unchecked(&self, buffer: &mut impl BufMut) { + buffer.put_u32(0); // unused field + } + + fn data_block(&self) -> Bytes { + self.offending_packet.clone() + } + + fn code(&self) -> u8 { + self.code.into() + } + + // Question(mlegner): Should we allow unknown codes? + fn check_code(_code: u8) -> Result<(), ScmpDecodeError> { + Ok(()) + } + + fn from_raw_unchecked(value: ScmpMessageRaw) -> Self { + let mut offending_packet = value.payload.clone(); + offending_packet.advance(Self::INFO_BLOCK_LENGTH); // unused field + Self { + code: DestinationUnreachableCode::from(value.code), + offending_packet, + } + } +} + +error_message!( + /// Error sent in response to a packet that cannot be forwarded because it is larger than the + /// MTU of the outgoing link. + pub struct ScmpPacketTooBig: PacketTooBig { + /// The Maximum Transmission Unit of the next-hop link. + pub mtu: u16, + } +); +impl ScmpMessageEncodeDecode for ScmpPacketTooBig { + const INFO_BLOCK_LENGTH: usize = 2; + + fn encode_info_block_unchecked(&self, buffer: &mut impl BufMut) { + buffer.put_u16(0); // reserved field + buffer.put_u16(self.mtu); + } + + fn info_block_checksum<'a>( + &self, + base_digest: &'a mut ChecksumDigest, + ) -> &'a mut ChecksumDigest { + base_digest.add_u16(self.mtu) + } + + fn data_block(&self) -> Bytes { + self.offending_packet.clone() + } + + fn from_raw_unchecked(value: ScmpMessageRaw) -> Self { + let mut payload = value.payload; + payload.advance(2); // reserved field + let mtu = payload.get_u16(); + Self { + mtu, + offending_packet: payload, + } + } +} + +encoded_type!( + #[allow(missing_docs)] + pub enum ParameterProblemCode(u8) { + ErroneousHeaderField = 0, + UnknownNextHdrType = 1, + InvalidCommonHeader = 16, + UnknownScionVersion = 17, + FlowIdRequired = 18, + InvalidPacketSize = 19, + UnknownPathType = 20, + UnknownAddressFormat = 21, + InvalidAddressHeader = 32, + InvalidSourceAddress = 33, + InvalidDestinationAddress = 34, + NonLocalDelivery = 35, + InvalidPath = 48, + UnknownHopFieldConsIngressInterface = 49, + UnknownHopFieldConsEgressInterface = 50, + InvalidHopFieldMac = 51, + PathExpired = 52, + InvalidSegmentChange = 53, + InvalidExtensionHeader = 64, + UnknownHopByHopOption = 65, + UnknownEndToEndOption = 66; + Unassigned = _, + } +); +error_message!( + /// Error sent by an on-path AS in response to a packet with problems in any of the SCION + /// headers. + pub struct ScmpParameterProblem: ParameterProblem { + code: ParameterProblemCode, + } +); +impl ScmpMessageEncodeDecode for ScmpParameterProblem { + const INFO_BLOCK_LENGTH: usize = 0; + + fn data_block(&self) -> Bytes { + self.offending_packet.clone() + } + + fn code(&self) -> u8 { + self.code.into() + } + + fn check_code(_code: u8) -> Result<(), ScmpDecodeError> { + Ok(()) + } + + fn from_raw_unchecked(value: ScmpMessageRaw) -> Self { + Self { + code: ParameterProblemCode::from(value.code), + offending_packet: value.payload, + } + } +} + +error_message!( + /// Error sent by a router in response to a packet that cannot be forwarded because the link to + /// an external AS broken. + pub struct ScmpExternalInterfaceDown: ExternalInterfaceDown { + /// The interface ID of the external link with connectivity issue. + /// + /// If the actual ID is shorter than 64 bits, it is stored in the least-significant bits + /// of this field. + pub interface_id: u64, + } +); +impl ScmpMessageEncodeDecode for ScmpExternalInterfaceDown { + const INFO_BLOCK_LENGTH: usize = 8; + + fn encode_info_block_unchecked(&self, buffer: &mut impl BufMut) { + buffer.put_u64(self.interface_id); + } + + fn info_block_checksum<'a>( + &self, + base_digest: &'a mut ChecksumDigest, + ) -> &'a mut ChecksumDigest { + base_digest.add_u64(self.interface_id) + } + + fn data_block(&self) -> Bytes { + self.offending_packet.clone() + } + + fn from_raw_unchecked(value: ScmpMessageRaw) -> Self { + let mut payload = value.payload; + Self { + interface_id: payload.get_u64(), + offending_packet: payload, + } + } +} + +error_message!( + /// Error sent by a router in response to a packet that cannot be forwarded inside the AS + /// because the connectivity between the ingress and egress routers is broken. + pub struct ScmpInternalConnectivityDown: InternalConnectivityDown { + /// The interface ID of the ingress link. + /// + /// If the actual ID is shorter than 64 bits, it is stored in the least-significant bits + /// of this field. + pub ingress_interface_id: u64, + /// The interface ID of the egress link. + /// + /// If the actual ID is shorter than 64 bits, it is stored in the least-significant bits + /// of this field. + pub egress_interface_id: u64, + } +); +impl ScmpMessageEncodeDecode for ScmpInternalConnectivityDown { + const INFO_BLOCK_LENGTH: usize = 8; + + fn encode_info_block_unchecked(&self, buffer: &mut impl BufMut) { + buffer.put_u64(self.ingress_interface_id); + buffer.put_u64(self.egress_interface_id); + } + + fn info_block_checksum<'a>( + &self, + base_digest: &'a mut ChecksumDigest, + ) -> &'a mut ChecksumDigest { + base_digest + .add_u64(self.ingress_interface_id) + .add_u64(self.egress_interface_id) + } + + fn data_block(&self) -> Bytes { + self.offending_packet.clone() + } + + fn from_raw_unchecked(value: ScmpMessageRaw) -> Self { + let mut payload = value.payload; + Self { + ingress_interface_id: payload.get_u64(), + egress_interface_id: payload.get_u64(), + offending_packet: payload, + } + } +} + +/// Trait implemented by all SCMP informational messages. +pub trait ScmpInformationalMessage { + /// Get the message's identifier. + fn get_identifier(&self) -> u16; + /// Get the message's sequence number. + fn get_sequence_number(&self) -> u16; + + /// Get the combination of the message's identifier and sequence number. + /// + /// This can be used to match reply messages to their corresponding requests. + fn get_identifier_and_sequence_number(&self) -> u32 { + (self.get_identifier() as u32) << 16 | self.get_sequence_number() as u32 + } + + /// Encodes the identifier and sequence number to the provided buffer. + fn encode_identifier_and_sequence_number_unchecked(&self, buffer: &mut impl BufMut) { + buffer.put_u16(self.get_identifier()); + buffer.put_u16(self.get_sequence_number()); + } +} + +macro_rules! informational_message { + ( + $(#[$outer:meta])* + pub struct $name:ident : $message_type:ident {$($(#[$doc:meta])* $vis:vis $field:ident : $type:ty,)*} + ) => { + // TODO(mlegner): Add examples to automatically test some properties. + $(#[$outer])* + #[derive(Debug, Clone, PartialEq)] + pub struct $name { + /// A 16-bit identifier to aid matching replies with requests. + pub identifier: u16, + /// A 16-bit sequence number to aid matching replies with requests. + pub sequence_number: u16, + $($(#[$doc])* $vis $field: $type,)* + } + + impl ScmpInformationalMessage for $name { + #[inline] + fn get_identifier(&self) -> u16 { + self.identifier + } + + #[inline] + fn get_sequence_number(&self) -> u16 { + self.sequence_number + } + } + + impl_conversion_and_type!($name: $message_type); + }; +} + +macro_rules! impl_echo_request_and_reply { + ($name:ident) => { + impl ScmpMessageEncodeDecode for $name { + const INFO_BLOCK_LENGTH: usize = 4; + + fn encode_info_block_unchecked(&self, buffer: &mut impl BufMut) { + self.encode_identifier_and_sequence_number_unchecked(buffer) + } + + fn info_block_checksum<'a>( + &self, + base_digest: &'a mut ChecksumDigest, + ) -> &'a mut ChecksumDigest { + base_digest + .add_u16(self.get_identifier()) + .add_u16(self.get_sequence_number()) + } + + fn data_block(&self) -> Bytes { + self.data.clone() + } + + fn from_raw_unchecked(value: ScmpMessageRaw) -> Self { + let mut payload = value.payload; + Self { + identifier: payload.get_u16(), + sequence_number: payload.get_u16(), + data: payload, + } + } + } + }; +} + +informational_message!( + /// Echo request to the destination to support ping functionality, equivalent to the + /// corresponding ICMP message. + pub struct ScmpEchoRequest: EchoRequest { + /// Arbitrary data to be echoed by the destination. + pub data: Bytes, + } +); +impl_echo_request_and_reply!(ScmpEchoRequest); + +informational_message!( + /// Echo reply to support ping functionality, equivalent to the corresponding ICMP message. + pub struct ScmpEchoReply: EchoReply { + /// The data of the corresponding [`ScmpEchoRequest`]. + pub data: Bytes, + } +); +impl_echo_request_and_reply!(ScmpEchoReply); + +/// The position of a hop-field interface. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum InterfacePosition { + /// Ingress interface in forwarding direction. + Ingress, + /// Egress interface in forwarding direction. + Egress, +} +informational_message!( + /// Request to an on-path router to support traceroute functionality. + pub struct ScmpTracerouteRequest: TracerouteRequest {} +); +impl ScmpMessageEncodeDecode for ScmpTracerouteRequest { + const INFO_BLOCK_LENGTH: usize = 20; + + fn encode_info_block_unchecked(&self, buffer: &mut impl BufMut) { + self.encode_identifier_and_sequence_number_unchecked(buffer); + buffer.put_bytes(0, Self::INFO_BLOCK_LENGTH - 4) + } + + fn info_block_checksum<'a>( + &self, + base_digest: &'a mut ChecksumDigest, + ) -> &'a mut ChecksumDigest { + base_digest + .add_u16(self.get_identifier()) + .add_u16(self.get_sequence_number()) + } + + fn from_raw_unchecked(value: ScmpMessageRaw) -> Self { + let mut payload = value.payload; + Self { + identifier: payload.get_u16(), + sequence_number: payload.get_u16(), + } + } +} + +informational_message!( + /// Reply by an on-path router to support traceroute functionality. + pub struct ScmpTracerouteReply: TracerouteReply { + /// The ISD-AS number of the originating router. + pub isd_asn: IsdAsn, + /// The interface ID of the originating router. + /// + /// If the actual ID is shorter than 64 bits, it is stored in the least-significant bits + /// of this field. + pub interface_id: u64, + } +); +impl ScmpMessageEncodeDecode for ScmpTracerouteReply { + const INFO_BLOCK_LENGTH: usize = 20; + + fn encode_info_block_unchecked(&self, buffer: &mut impl BufMut) { + self.encode_identifier_and_sequence_number_unchecked(buffer); + buffer.put_u64(self.isd_asn.into()); + buffer.put_u64(self.interface_id); + } + + fn info_block_checksum<'a>( + &self, + base_digest: &'a mut ChecksumDigest, + ) -> &'a mut ChecksumDigest { + base_digest + .add_u16(self.get_identifier()) + .add_u16(self.get_sequence_number()) + .add_u64(self.isd_asn.into()) + .add_u64(self.interface_id) + } + + fn from_raw_unchecked(value: ScmpMessageRaw) -> Self { + let mut payload = value.payload; + Self { + identifier: payload.get_u16(), + sequence_number: payload.get_u16(), + isd_asn: payload.get_u64().into(), + interface_id: payload.get_u64(), + } + } +} + +encoded_type!( + /// SCMP message types. + /// + /// For the supported types (all except [`Self::OtherError`] and [`Self::OtherInfo`]) further + /// documentation is provided by the corresponding `Scmp*` structs. + #[allow(missing_docs)] + pub enum ScmpType(u8) { + DestinationUnreachable = 1, + PacketTooBig = 2, + ParameterProblem = 4, + ExternalInterfaceDown = 5, + InternalConnectivityDown = 6, + EchoRequest = 128, + EchoReply = 129, + TracerouteRequest = 130, + TracerouteReply = 131; + OtherError = 0..=Self::MAX_VALUE_ERROR, + OtherInfo = Self::MIN_VALUE_INFORMATIONAL.., + } +); + +impl ScmpType { + const MAX_VALUE_ERROR: u8 = 127; + const MIN_VALUE_INFORMATIONAL: u8 = Self::MAX_VALUE_ERROR + 1; + + /// Returns true iff the type represents an error. + pub fn is_error(&self) -> bool { + u8::from(*self) <= Self::MAX_VALUE_ERROR + } + + /// Returns true iff the type represents an informational message. + pub fn is_informational(&self) -> bool { + u8::from(*self) >= Self::MIN_VALUE_INFORMATIONAL + } + + /// Returns true for all supported SCMP types and false otherwise. + pub fn is_supported(&self) -> bool { + !matches!(self, Self::OtherError(_) | Self::OtherInfo(_)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scmp_type_consistent() { + for value in 0..u8::MAX { + let scmp_type = ScmpType::from(value); + assert_eq!(u8::from(scmp_type), value); + assert!(scmp_type.is_error() ^ scmp_type.is_informational()); + } + } +} diff --git a/crates/scion-proto/src/scmp/raw.rs b/crates/scion-proto/src/scmp/raw.rs new file mode 100644 index 0000000..e9e3f9d --- /dev/null +++ b/crates/scion-proto/src/scmp/raw.rs @@ -0,0 +1,120 @@ +//! Format, decoding, and encoding of general (raw) SCMP messages. + +use bytes::{Buf, BufMut, Bytes}; + +use super::{ScmpDecodeError, ScmpType}; +use crate::{ + packet::{AddressHeader, ChecksumDigest, InadequateBufferSize}, + wire_encoding::{WireDecode, WireEncodeVec}, +}; + +/// Format of an SCMP message. +/// +/// See the [SCION documentation page][scion-doc-scmp] for further details. +/// +/// The optional and variable-length `InfoBlock` and `DataBlock` are here represented by a single +/// field [`Self::payload`]. +/// +/// [scion-doc-scmp]: https://docs.scion.org/en/latest/protocols/scmp.html +#[derive(Debug, Clone, PartialEq)] +pub struct ScmpMessageRaw { + /// The type of the SCMP message. + /// + /// This determines the format and content of the [`Self::payload`]. + pub message_type: ScmpType, + /// Additional granularity to the [`Self::message_type`]. + pub code: u8, + /// Checksum to detect accidental data corruption. + pub checksum: u16, + /// Optional field of variable length combining the `InfoBlock` and `DataBlock`. + /// + /// The format depends on [`Self::message_type`]. + pub payload: Bytes, +} + +impl ScmpMessageRaw { + /// SCION protocol number for SCMP. + /// + /// See the [IETF SCION-dataplane RFC draft][rfc] for possible values. + /// + ///[rfc]: https://www.ietf.org/archive/id/draft-dekater-scion-dataplane-00.html#protnum + pub const PROTOCOL_NUMBER: u8 = 202; + /// The length of the fixed fields in every SCMP message in bytes. + pub const FIELD_LENGTH: usize = 4; + + /// Returns the SCMP type of this message. + pub fn get_type(&self) -> ScmpType { + self.message_type + } + + /// Compute the checksum for this SCMP message using the provided address header. + pub fn calculate_checksum(&self, address_header: &AddressHeader) -> u16 { + ChecksumDigest::with_pseudoheader( + address_header, + Self::PROTOCOL_NUMBER, + self.total_length() + .try_into() + .expect("this never returns anything above `u32::MAX`"), + ) + .add_u16((u8::from(self.message_type) as u16) << 8 | self.code as u16) + .add_u16(self.checksum) + .add_slice(self.payload.as_ref()) + .checksum() + } + + /// Returns true if the checksum successfully verifies, otherwise false. + pub fn verify_checksum(&self, address_header: &AddressHeader) -> bool { + self.calculate_checksum(address_header) == 0 + } + + /// Clears then sets the checksum to the value returned by [`Self::calculate_checksum()`]. + pub fn set_checksum(&mut self, address_header: &AddressHeader) { + self.checksum = 0; + self.checksum = self.calculate_checksum(address_header); + } +} + +impl WireEncodeVec<2> for ScmpMessageRaw { + type Error = InadequateBufferSize; + + fn encode_with_unchecked(&self, buffer: &mut bytes::BytesMut) -> [Bytes; 2] { + buffer.put_u8(self.message_type.into()); + buffer.put_u8(self.code); + buffer.put_u16(self.checksum); + [buffer.split().freeze(), self.payload.clone()] + } + + #[inline] + fn total_length(&self) -> usize { + Self::FIELD_LENGTH + self.payload.len() + } + + #[inline] + fn required_capacity(&self) -> usize { + Self::FIELD_LENGTH + } +} + +impl WireDecode for ScmpMessageRaw { + type Error = ScmpDecodeError; + + /// Interpret all data beyond the fixed fields as the message payload. Length and format checks + /// are only applied when converting to an [`ScmpMessage`][super::ScmpMessage]. + fn decode(data: &mut T) -> Result { + if data.remaining() < Self::FIELD_LENGTH { + return Err(ScmpDecodeError::MessageEmptyOrTruncated); + } + + let message_type = data.get_u8().into(); + let code = data.get_u8(); + let checksum = data.get_u16(); + let payload = data.copy_to_bytes(data.remaining()); + + Ok(Self { + message_type, + code, + checksum, + payload, + }) + } +}