From 836e9e9df29fcb2687d4bb21b84ae52bbaa2e552 Mon Sep 17 00:00:00 2001 From: Markus Legner Date: Fri, 22 Dec 2023 10:22:57 +0100 Subject: [PATCH] feat: add SCION SCMP packet definitions --- .../scion-proto/src/address/scion_address.rs | 14 +- .../scion-proto/src/address/socket_address.rs | 9 ++ crates/scion-proto/src/datagram.rs | 14 +- crates/scion-proto/src/packet.rs | 3 + crates/scion-proto/src/packet/error.rs | 23 ++- crates/scion-proto/src/packet/headers.rs | 44 ++++-- crates/scion-proto/src/packet/raw.rs | 4 +- crates/scion-proto/src/packet/scmp.rs | 138 ++++++++++++++++++ crates/scion-proto/src/packet/udp.rs | 13 +- crates/scion-proto/src/path/standard.rs | 46 +++++- crates/scion-proto/src/scmp/error.rs | 5 + crates/scion-proto/src/scmp/messages.rs | 67 +++++++-- 12 files changed, 344 insertions(+), 36 deletions(-) create mode 100644 crates/scion-proto/src/packet/scmp.rs diff --git a/crates/scion-proto/src/address/scion_address.rs b/crates/scion-proto/src/address/scion_address.rs index 7d25ac0..3fd4ce6 100644 --- a/crates/scion-proto/src/address/scion_address.rs +++ b/crates/scion-proto/src/address/scion_address.rs @@ -3,6 +3,7 @@ use std::net::{Ipv4Addr, Ipv6Addr}; use super::{error::AddressKind, AddressParseError, HostAddr, IsdAsn, ServiceAddr}; +use crate::packet::AddressInfo; /// A SCION network address. /// @@ -29,7 +30,7 @@ impl ScionAddr { } } - /// Returns the host associated with this socket address. + /// Returns the host associated with this SCION address. pub const fn host(&self) -> HostAddr { match self { ScionAddr::V4(addr) => HostAddr::V4(*addr.host()), @@ -60,6 +61,15 @@ impl ScionAddr { ScionAddr::Svc(addr) => addr.set_isd_asn(new_isd_asn), } } + + /// Returns the address info corresponding to the SCION address's address type. + pub const fn address_info(&self) -> AddressInfo { + match self { + ScionAddr::V4(_) => AddressInfo::IPV4, + ScionAddr::V6(_) => AddressInfo::IPV6, + ScionAddr::Svc(_) => AddressInfo::SERVICE, + } + } } impl AsRef for ScionAddr { @@ -138,7 +148,7 @@ macro_rules! scion_address { Self { isd_asn, host } } - /// Returns the host associated with this socket address. + /// Returns the host associated with this SCION address. pub const fn host(&self) -> &$host_type { &self.host } diff --git a/crates/scion-proto/src/address/socket_address.rs b/crates/scion-proto/src/address/socket_address.rs index e19c0da..3b95d8a 100644 --- a/crates/scion-proto/src/address/socket_address.rs +++ b/crates/scion-proto/src/address/socket_address.rs @@ -57,6 +57,15 @@ impl SocketAddr { } } + /// Returns the SCION address associated with this socket address. + pub fn scion_address(&self) -> ScionAddr { + match self { + SocketAddr::V4(addr) => ScionAddr::V4(*addr.scion_addr()), + SocketAddr::V6(addr) => ScionAddr::V6(*addr.scion_addr()), + SocketAddr::Svc(addr) => ScionAddr::Svc(*addr.scion_addr()), + } + } + /// Returns the host address associated with this socket address. pub fn host(&self) -> HostAddr { match self { diff --git a/crates/scion-proto/src/datagram.rs b/crates/scion-proto/src/datagram.rs index 150e7df..683115c 100644 --- a/crates/scion-proto/src/datagram.rs +++ b/crates/scion-proto/src/datagram.rs @@ -3,7 +3,14 @@ use bytes::{Buf, BufMut, Bytes, BytesMut}; use crate::{ - packet::{AddressHeader, ByEndpoint, ChecksumDigest, InadequateBufferSize, MessageChecksum}, + packet::{ + self, + AddressHeader, + ByEndpoint, + ChecksumDigest, + InadequateBufferSize, + MessageChecksum, + }, wire_encoding::{WireDecode, WireEncodeVec}, }; @@ -14,6 +21,9 @@ pub enum UdpDecodeError { DatagramEmptyOrTruncated, #[error("next-header value of SCION header is not correct")] WrongProtocolNumber(u8), + /// An error when decoding the SCION packet. + #[error(transparent)] + PackedDecodeError(#[from] packet::DecodeError), } #[allow(missing_docs)] @@ -29,7 +39,7 @@ pub enum UdpEncodeError { /// the [RFC]. /// /// [RFC]: https://www.ietf.org/archive/id/draft-dekater-scion-dataplane-00.html -#[derive(Debug, Default, PartialEq)] +#[derive(Debug, Default, PartialEq, Clone)] pub struct UdpMessage { /// The source and destination ports pub port: ByEndpoint, diff --git a/crates/scion-proto/src/packet.rs b/crates/scion-proto/src/packet.rs index c9db99b..99988ee 100644 --- a/crates/scion-proto/src/packet.rs +++ b/crates/scion-proto/src/packet.rs @@ -23,6 +23,9 @@ pub use headers::{ pub mod raw; pub use raw::ScionPacketRaw; +pub mod scmp; +pub use scmp::ScionPacketScmp; + pub mod udp; pub use udp::ScionPacketUdp; diff --git a/crates/scion-proto/src/packet/error.rs b/crates/scion-proto/src/packet/error.rs index 6741b9f..ab1285e 100644 --- a/crates/scion-proto/src/packet/error.rs +++ b/crates/scion-proto/src/packet/error.rs @@ -28,16 +28,35 @@ impl From for DecodeError { } } -/// Errors raised when failing to encode a [`super::ScionPacketRaw`] or [`super::ScionPacketUdp`]. -#[allow(missing_docs)] +/// Errors raised when failing to encode a [`super::ScionPacketRaw`], [`super::ScionPacketScmp`], or +/// [`super::ScionPacketUdp`]. #[derive(Debug, thiserror::Error, PartialEq, Eq, Clone, Copy)] pub enum EncodeError { + /// The payload is too large to be properly encoded in a SCION packet. #[error("packet payload is too large")] PayloadTooLarge, + /// The overall header is too large. + /// + /// This is most likely due to a too long path. #[error("packet header is too large")] HeaderTooLarge, } +/// Errors raised when creating a [`super::ScionPacketScmp`]. +#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone, Copy)] +pub enum ScmpEncodeError { + /// Some SCMP messages (notably the [`ScmpTracerouteRequest`][crate::scmp::ScmpTracerouteRequest]) + /// require a specific path type. + #[error("the provided path type is not appropriate for this type of packet")] + InappropriatePathType, + /// A provided parameter is out of range. + #[error("a provided parameter is out of range")] + ParameterOutOfRange, + /// A general [`EncodeError`] occurred. + #[error("encoding error")] + GeneralEncodeError(#[from] EncodeError), +} + /// Raised if the buffer does not have sufficient capacity for encoding the SCION headers. /// /// As the headers can be a maximum of 1020 bytes in length, it is advisable to have at diff --git a/crates/scion-proto/src/packet/headers.rs b/crates/scion-proto/src/packet/headers.rs index 65e7836..bf9ac0f 100644 --- a/crates/scion-proto/src/packet/headers.rs +++ b/crates/scion-proto/src/packet/headers.rs @@ -11,13 +11,13 @@ pub use address_header::{AddressHeader, RawHostAddress}; use super::{EncodeError, InadequateBufferSize}; use crate::{ - address::SocketAddr, + address::{ScionAddr, SocketAddr}, path::{DataplanePath, Path}, wire_encoding::WireEncode, }; /// SCION packet headers. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ScionHeaders { /// Metadata about the remaining headers and payload. pub common: CommonHeader, @@ -28,10 +28,10 @@ pub struct ScionHeaders { } impl ScionHeaders { - /// Creates a new [`ScionHeaders`] object given the source and destination [`SocketAddr`], - /// the [`Path`], the next-header value, and the payload length + /// Creates a new [`ScionHeaders`] object given the source and destination [`ScionAddr`], + /// the [`Path`], the next-header value, and the payload length. pub fn new( - endhosts: &ByEndpoint, + endhosts: &ByEndpoint, path: &Path, next_header: u8, payload_length: usize, @@ -51,14 +51,14 @@ impl ScionHeaders { let common_header = CommonHeader { version: Version::default(), traffic_class: 0, - flow_id: Self::simple_flow_id(endhosts), + flow_id: Self::dummy_flow_id(), next_header, header_length_factor, payload_length: payload_length .try_into() .map_err(|_| EncodeError::PayloadTooLarge)?, path_type: path.dataplane_path.path_type(), - address_info: endhosts.map(SocketAddr::address_info), + address_info: endhosts.map(ScionAddr::address_info), reserved: 0, }; @@ -69,10 +69,34 @@ impl ScionHeaders { }) } - // TODO(mlegner): More sophisticated flow ID? + /// Creates a new [`ScionHeaders`] object given the source and destination [`SocketAddr`], + /// the [`Path`], the next-header value, and the payload length. + pub fn new_with_ports( + endhosts: &ByEndpoint, + path: &Path, + next_header: u8, + payload_length: usize, + ) -> Result { + let mut headers = Self::new( + &endhosts.map(SocketAddr::scion_address), + path, + next_header, + payload_length, + )?; + headers.common.flow_id = Self::simple_flow_id_with_ports(endhosts); + Ok(headers) + } + + // TODO(mlegner): Figure out better way of setting the flow ID for various cases. + + /// Set the flow ID to a dummy value of 1. + const fn dummy_flow_id() -> FlowId { + FlowId::new_unchecked(1) + } + /// Simple flow ID containing the XOR of source and destination port with a prepended 1 /// to prevent a value of 0. - fn simple_flow_id(endhosts: &ByEndpoint) -> FlowId { + fn simple_flow_id_with_ports(endhosts: &ByEndpoint) -> FlowId { (0x1_0000 | (endhosts.source.port() ^ endhosts.destination.port()) as u32).into() } } @@ -160,7 +184,7 @@ mod tests { source: SocketAddr::from_str("[1-1,10.0.0.1]:10001").unwrap(), destination: SocketAddr::from_str("[1-2,10.0.0.2]:10002").unwrap(), }; - let headers = ScionHeaders::new( + let headers = ScionHeaders::new_with_ports( &endpoints, &Path::empty(endpoints.map(SocketAddr::isd_asn)), 0, diff --git a/crates/scion-proto/src/packet/raw.rs b/crates/scion-proto/src/packet/raw.rs index af9a4b7..e5b1cc0 100644 --- a/crates/scion-proto/src/packet/raw.rs +++ b/crates/scion-proto/src/packet/raw.rs @@ -18,7 +18,7 @@ use crate::{ }; /// A SCION network packet. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ScionPacketRaw { /// Packet headers pub headers: ScionHeaders, @@ -34,7 +34,7 @@ impl ScionPacketRaw { payload: Bytes, next_header: u8, ) -> Result { - let headers = ScionHeaders::new(endhosts, path, next_header, payload.len())?; + let headers = ScionHeaders::new_with_ports(endhosts, path, next_header, payload.len())?; Ok(Self { headers, payload }) } diff --git a/crates/scion-proto/src/packet/scmp.rs b/crates/scion-proto/src/packet/scmp.rs new file mode 100644 index 0000000..ab5ae81 --- /dev/null +++ b/crates/scion-proto/src/packet/scmp.rs @@ -0,0 +1,138 @@ +//! SCION packets containing SCMP messages. + +use bytes::{Buf, Bytes}; + +use super::{error::ScmpEncodeError, InadequateBufferSize, ScionHeaders, ScionPacketRaw}; +use crate::{ + address::ScionAddr, + packet::ByEndpoint, + path::{DataplanePath, Path}, + scmp::{ScmpDecodeError, ScmpMessage, ScmpTracerouteRequest, ScmpType, SCMP_PROTOCOL_NUMBER}, + wire_encoding::{WireDecode, WireEncodeVec}, +}; + +/// A SCION packet containing a SCMP message. +#[derive(Debug, Clone, PartialEq)] +pub struct ScionPacketScmp { + /// Packet headers + pub headers: ScionHeaders, + /// The contained SCMP message + pub message: ScmpMessage, +} + +impl ScionPacketScmp { + /// Returns the SCMP message type. + pub fn get_type(&self) -> ScmpType { + self.message.get_type() + } +} + +/// The position of a router for a given hop in forwarding direction. +pub enum RouterPosition { + /// The ingress router in forwarding direction. + Ingress, + /// The egress router in forwarding direction. + Egress, +} + +impl ScionPacketScmp { + /// Creates a new SCION SCMP packet based on the SCMP message correctly setting the checksum. + /// + /// This does not work for an [`ScmpTracerouteRequest`] message, which requires setting specific + /// router alert flags. Use [`Self::new_traceroute_request`] for this purpose. + pub fn new( + endhosts: &ByEndpoint, + path: &Path, + mut message: ScmpMessage, + ) -> Result { + let headers = + ScionHeaders::new(endhosts, path, SCMP_PROTOCOL_NUMBER, message.total_length())?; + message.set_checksum(&headers.address); + + Ok(Self { headers, message }) + } + + /// Creates a new SCION packet containing an [`ScmpTracerouteRequest`]. + /// + /// This correctly sets the appropriate router alert flags and checksum. + pub fn new_traceroute_request( + endhosts: &ByEndpoint, + path: &Path, + identifier: u16, + sequence_number: u16, + hop_index: usize, + router_position: RouterPosition, + ) -> Result { + let DataplanePath::Standard(ref standard_path) = path.dataplane_path else { + return Err(ScmpEncodeError::InappropriatePathType); + }; + let mut standard_path_mut = standard_path.to_mut(); + let Some(hop_field) = standard_path_mut.hop_field_mut(hop_index) else { + return Err(ScmpEncodeError::ParameterOutOfRange); + }; + match router_position { + RouterPosition::Ingress => hop_field.set_cons_ingress_router_alert(true), + RouterPosition::Egress => hop_field.set_cons_egress_router_alert(true), + }; + + let path = Path { + dataplane_path: DataplanePath::Standard(standard_path_mut.freeze()), + underlay_next_hop: path.underlay_next_hop, + isd_asn: path.isd_asn, + metadata: None, + }; + let packet = Self::new( + endhosts, + &path, + ScmpTracerouteRequest::new(identifier, sequence_number).into(), + )?; + + Ok(packet) + } +} + +impl TryFrom for ScionPacketScmp { + type Error = ScmpDecodeError; + + fn try_from(mut value: ScionPacketRaw) -> Result { + if value.headers.common.next_header != SCMP_PROTOCOL_NUMBER { + return Err(ScmpDecodeError::WrongProtocolNumber( + value.headers.common.next_header, + )); + } + Ok(Self { + headers: value.headers, + message: ScmpMessage::decode(&mut value.payload)?, + }) + } +} + +impl WireDecode for ScionPacketScmp { + type Error = ScmpDecodeError; + + fn decode(data: &mut T) -> Result { + ScionPacketRaw::decode(data)?.try_into() + } +} + +impl WireEncodeVec<3> for ScionPacketScmp { + type Error = InadequateBufferSize; + + fn encode_with_unchecked(&self, buffer: &mut bytes::BytesMut) -> [Bytes; 3] { + let encoded_headers = self.headers.encode_with_unchecked(buffer); + let encoded_message = self.message.encode_with_unchecked(buffer); + [ + encoded_headers[0].clone(), + encoded_message[0].clone(), + encoded_message[1].clone(), + ] + } + + fn total_length(&self) -> usize { + self.headers.total_length() + self.message.total_length() + } + + fn required_capacity(&self) -> usize { + self.headers.required_capacity() + self.message.required_capacity() + } +} diff --git a/crates/scion-proto/src/packet/udp.rs b/crates/scion-proto/src/packet/udp.rs index 0de477a..02b401d 100644 --- a/crates/scion-proto/src/packet/udp.rs +++ b/crates/scion-proto/src/packet/udp.rs @@ -1,6 +1,6 @@ //! SCION packets containing UDP datagrams. -use bytes::Bytes; +use bytes::{Buf, Bytes}; use super::{InadequateBufferSize, MessageChecksum, ScionHeaders, ScionPacketRaw}; use crate::{ @@ -12,6 +12,7 @@ use crate::{ }; /// A SCION packet containing a UDP datagram. +#[derive(Debug, Clone, PartialEq)] pub struct ScionPacketUdp { /// Packet headers pub headers: ScionHeaders, @@ -59,7 +60,7 @@ impl ScionPacketUdp { path: &Path, payload: Bytes, ) -> Result { - let headers = ScionHeaders::new( + let headers = ScionHeaders::new_with_ports( endhosts, path, UdpMessage::PROTOCOL_NUMBER, @@ -89,6 +90,14 @@ impl TryFrom for ScionPacketUdp { } } +impl WireDecode for ScionPacketUdp { + type Error = UdpDecodeError; + + fn decode(data: &mut T) -> Result { + ScionPacketRaw::decode(data)?.try_into() + } +} + impl WireEncodeVec<3> for ScionPacketUdp { type Error = InadequateBufferSize; diff --git a/crates/scion-proto/src/path/standard.rs b/crates/scion-proto/src/path/standard.rs index 15bb8d7..993f8e6 100644 --- a/crates/scion-proto/src/path/standard.rs +++ b/crates/scion-proto/src/path/standard.rs @@ -1,7 +1,7 @@ //! A standard SCION path. -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; -use bytes::{Buf, BufMut, Bytes}; +use bytes::{Buf, BufMut, Bytes, BytesMut}; use chrono::{DateTime, Utc}; use super::DataplanePathErrorKind; @@ -173,7 +173,7 @@ where } /// Returns an iterator over all of the [`HopField`]s in the SCION path. - fn hop_fields(&self) -> HopFields { + pub fn hop_fields(&self) -> HopFields { self.hop_fields_subset(0, self.meta_header.hop_fields_count()) } @@ -196,6 +196,46 @@ where } } +impl StandardPath +where + T: DerefMut, +{ + /// Returns the [`HopField`] at the specified index as a mutable reference, if within range. + pub fn hop_field_mut(&mut self, index: usize) -> Option<&mut HopField> { + if index < self.meta_header.hop_fields_count() { + let start = self.meta_header.hop_field_offset(index); + let slice = &mut self.encoded_path[start..(start + HopField::LENGTH)]; + Some(HopField::new_mut(slice)) + } else { + None + } + } +} + +impl StandardPath { + /// Converts a standard path over an immutable reference to one over an mutable reference. + /// + /// This requires copying the encoded path. + pub fn to_mut(&self) -> StandardPath { + let mut encoded_path = BytesMut::with_capacity(self.encoded_path.len()); + encoded_path.extend_from_slice(self.encoded_path.as_ref()); + StandardPath { + meta_header: self.meta_header.clone(), + encoded_path, + } + } +} + +impl StandardPath { + /// Converts a standard path over a mutable reference to one over an immutable reference. + pub fn freeze(self) -> StandardPath { + StandardPath { + meta_header: self.meta_header, + encoded_path: self.encoded_path.freeze(), + } + } +} + impl<'b> StandardPath<&'b mut [u8]> { /// Converts a standard path over a mutable reference to one over an immutable reference. pub fn freeze(self) -> StandardPath<&'b [u8]> { diff --git a/crates/scion-proto/src/scmp/error.rs b/crates/scion-proto/src/scmp/error.rs index 4b70944..105343b 100644 --- a/crates/scion-proto/src/scmp/error.rs +++ b/crates/scion-proto/src/scmp/error.rs @@ -1,5 +1,7 @@ //! Errors encountered when handling SCMP messages. +use crate::packet; + /// Error encountered when attempting to decode an SCMP message. #[derive(Debug, thiserror::Error)] pub enum ScmpDecodeError { @@ -20,4 +22,7 @@ pub enum ScmpDecodeError { /// of the SCION header doesn't match [`SCMP_PROTOCOL_NUMBER`][super::SCMP_PROTOCOL_NUMBER]. #[error("next-header value of SCION header is not correct")] WrongProtocolNumber(u8), + /// An error when decoding the SCION packet. + #[error(transparent)] + PackedDecodeError(#[from] packet::DecodeError), } diff --git a/crates/scion-proto/src/scmp/messages.rs b/crates/scion-proto/src/scmp/messages.rs index 344e4b5..aea79a8 100644 --- a/crates/scion-proto/src/scmp/messages.rs +++ b/crates/scion-proto/src/scmp/messages.rs @@ -7,7 +7,7 @@ use crate::{ address::IsdAsn, packet::{AddressHeader, ChecksumDigest, InadequateBufferSize, MessageChecksum}, utils::encoded_type, - wire_encoding::WireEncodeVec, + wire_encoding::{WireDecode, WireEncodeVec}, }; /// Fully decoded SCMP message with an appropriate format. @@ -64,6 +64,24 @@ pub enum ScmpMessage { UnknownError(ScmpMessageRaw), } +macro_rules! call_method_on_scmp_variants { + ($self:ident.$name:ident($($param:ident),*)) => { + 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),*), + } + }; +} + +// Question(mlegner): Do you know a good method of matching both `&` and `&mut`? macro_rules! lift_fn_from_scmp_variants { ( $(#[$outer:meta])* @@ -71,18 +89,16 @@ macro_rules! lift_fn_from_scmp_variants { ) => { $(#[$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),*), - } + call_method_on_scmp_variants!(self.$name($($param),*)) + } + }; + ( + $(#[$outer:meta])* + $vis:vis fn $name:ident(&mut self $(,$param:ident : $param_type:ty)*) -> $return_type:ty + ) => { + $(#[$outer])* + $vis fn $name(&mut self $(,$param : $param_type)*) -> $return_type { + call_method_on_scmp_variants!(self.$name($($param),*)) } }; } @@ -103,6 +119,11 @@ impl ScmpMessage { pub fn verify_checksum(&self, address_header: &AddressHeader) -> bool ); + lift_fn_from_scmp_variants!( + /// Returns true if the checksum successfully verifies, otherwise false. + pub fn set_checksum(&mut self, address_header: &AddressHeader) -> () + ); + /// Returns true iff `self` is an error message. pub fn is_error(&self) -> bool { self.get_type().is_error() @@ -151,6 +172,14 @@ impl TryFrom for ScmpMessage { } } +impl WireDecode for ScmpMessage { + type Error = ScmpDecodeError; + + fn decode(data: &mut Bytes) -> Result { + ScmpMessageRaw::decode(data)?.try_into() + } +} + impl WireEncodeVec<2> for ScmpMessage { type Error = InadequateBufferSize; @@ -242,6 +271,18 @@ macro_rules! impl_conversion_and_type { } } + impl TryFrom for $name { + type Error = ScmpDecodeError; + + fn try_from(value: ScmpMessage) -> Result { + if let ScmpMessage::$message_type(m) = value { + Ok(m) + } else { + Err(ScmpDecodeError::MessageTypeMismatch) + } + } + } + impl From<$name> for ScmpMessage { fn from(value: $name) -> Self { ScmpMessage::$message_type(value)