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 b0becd6..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}, + 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, @@ -65,9 +75,19 @@ impl UdpMessage { }; Ok(datagram) } +} + +impl MessageChecksum for UdpMessage { + fn checksum(&self) -> u16 { + self.checksum + } + + fn set_checksum(&mut self, address_header: &AddressHeader) { + self.checksum = 0; + self.checksum = self.calculate_checksum(address_header); + } - /// Compute the checksum for this datagram using the provided address header. - pub fn calculate_checksum(&self, address_header: &AddressHeader) -> u16 { + fn calculate_checksum(&self, address_header: &AddressHeader) -> u16 { ChecksumDigest::with_pseudoheader(address_header, Self::PROTOCOL_NUMBER, self.length.into()) .add_u16(self.port.source) .add_u16(self.port.destination) @@ -76,17 +96,6 @@ impl UdpMessage { .add_slice(&self.payload) .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 UdpMessage { diff --git a/crates/scion-proto/src/packet.rs b/crates/scion-proto/src/packet.rs index c2834fd..99988ee 100644 --- a/crates/scion-proto/src/packet.rs +++ b/crates/scion-proto/src/packet.rs @@ -23,8 +23,11 @@ pub use headers::{ pub mod raw; pub use raw::ScionPacketRaw; +pub mod scmp; +pub use scmp::ScionPacketScmp; + pub mod udp; pub use udp::ScionPacketUdp; mod checksum; -pub use checksum::ChecksumDigest; +pub use checksum::{ChecksumDigest, MessageChecksum}; diff --git a/crates/scion-proto/src/packet/checksum.rs b/crates/scion-proto/src/packet/checksum.rs index 1eb5d30..9d96bd7 100644 --- a/crates/scion-proto/src/packet/checksum.rs +++ b/crates/scion-proto/src/packet/checksum.rs @@ -6,6 +6,23 @@ use crate::{ wire_encoding::{MaybeEncoded, WireEncode}, }; +/// Trait implemented by all higher-layer messages that use 2-byte checksums. +pub trait MessageChecksum { + /// Returns the currently stored checksum of the message. + fn checksum(&self) -> u16; + + /// Clears then sets the checksum to the value returned by [`Self::calculate_checksum()`]. + fn set_checksum(&mut self, address_header: &AddressHeader); + + /// Compute the checksum for this message using the provided address header. + fn calculate_checksum(&self, address_header: &AddressHeader) -> u16; + + /// Returns true if the checksum successfully verifies, otherwise false. + fn verify_checksum(&self, address_header: &AddressHeader) -> bool { + self.calculate_checksum(address_header) == 0 + } +} + /// Incrementally computes the 16-bit checksum for upper layer protocols. /// /// A new, empty digest can be created with [`ChecksumDigest::new()`], or 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..d1a44ef 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,13 +28,14 @@ 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, + flow_id: FlowId, ) -> Result { let address_header = AddressHeader::from(*endhosts); @@ -51,14 +52,14 @@ impl ScionHeaders { let common_header = CommonHeader { version: Version::default(), traffic_class: 0, - flow_id: Self::simple_flow_id(endhosts), + 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,11 +70,24 @@ impl ScionHeaders { }) } - // TODO(mlegner): More sophisticated flow ID? - /// 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 { - (0x1_0000 | (endhosts.source.port() ^ endhosts.destination.port()) as u32).into() + /// Creates a new [`ScionHeaders`] object given the source and destination [`SocketAddr`], + /// the [`Path`], the next-header value, and the payload length. + /// + /// This is equivalent to [`ScionHeaders::new`] but uses [`FlowId::new_from_ports`] to set the + /// `flow_id`. + pub fn new_with_ports( + endhosts: &ByEndpoint, + path: &Path, + next_header: u8, + payload_length: usize, + ) -> Result { + Self::new( + &endhosts.map(SocketAddr::scion_address), + path, + next_header, + payload_length, + FlowId::new_from_ports(&endhosts.map(SocketAddr::port)), + ) } } @@ -160,7 +174,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/headers/common_header.rs b/crates/scion-proto/src/packet/headers/common_header.rs index 65a5b17..11c8de8 100644 --- a/crates/scion-proto/src/packet/headers/common_header.rs +++ b/crates/scion-proto/src/packet/headers/common_header.rs @@ -122,6 +122,23 @@ wire_encoding::bounded_uint! { pub struct FlowId(u32 : 20); } +// TODO(mlegner): Figure out better way of setting the flow ID for various cases. + +impl Default for FlowId { + /// Set the flow ID to a dummy value of 1 (0 is disallowed by the specification). + fn default() -> Self { + FlowId::new_unchecked(1) + } +} + +impl FlowId { + /// Simple flow ID containing the XOR of source and destination port with a prepended 1 + /// to prevent a (disallowed) value of 0. + pub fn new_from_ports(ports: &ByEndpoint) -> Self { + (0x1_0000 | (ports.source ^ ports.destination) as u32).into() + } +} + wire_encoding::bounded_uint!( /// An AddressInfo instance describes the type and length of a host address in /// the SCION common header. 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..2bced74 --- /dev/null +++ b/crates/scion-proto/src/packet/scmp.rs @@ -0,0 +1,150 @@ +//! SCION packets containing SCMP messages. + +use bytes::{Buf, Bytes}; + +use super::{error::ScmpEncodeError, FlowId, 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() + } +} + +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(), + FlowId::default(), + )?; + 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. + /// + /// The parameter `interface_index` corresponds to the actually traversed AS-level interfaces + /// on the path following the same conventions as the + /// [`PathMetadata`][crate::path::PathMetadata] (starting with `0` as the egress interface of + /// the local AS). + /// + /// # Errors + /// + /// Returns an [`ScmpEncodeError`] if the encoding of the packet headers fails or the + /// `interface_index` is out of range. + pub fn new_traceroute_request( + endhosts: &ByEndpoint, + path: &Path, + identifier: u16, + sequence_number: u16, + interface_index: usize, + ) -> Result { + let DataplanePath::Standard(ref standard_path) = path.dataplane_path else { + return Err(ScmpEncodeError::InappropriatePathType); + }; + let mut standard_path = standard_path.to_mut(); + + let Some(hop_field) = standard_path.hop_field_mut( + standard_path + .meta_header() + .hop_field_index_for_interface(interface_index), + ) else { + return Err(ScmpEncodeError::ParameterOutOfRange); + }; + if interface_index % 2 == 0 { + hop_field.set_cons_egress_router_alert(true); + } else { + hop_field.set_cons_ingress_router_alert(true); + }; + + let path = Path { + dataplane_path: DataplanePath::Standard(standard_path.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 7f839d9..02b401d 100644 --- a/crates/scion-proto/src/packet/udp.rs +++ b/crates/scion-proto/src/packet/udp.rs @@ -1,8 +1,8 @@ //! SCION packets containing UDP datagrams. -use bytes::Bytes; +use bytes::{Buf, Bytes}; -use super::{InadequateBufferSize, ScionHeaders, ScionPacketRaw}; +use super::{InadequateBufferSize, MessageChecksum, ScionHeaders, ScionPacketRaw}; use crate::{ address::SocketAddr, datagram::{UdpDecodeError, UdpMessage}, @@ -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..c457ccd 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; @@ -36,6 +36,9 @@ pub struct StandardPath { encoded_path: T, } +/// The standard SCION path header with a mutable encoded path. +pub type StandardPathMut = StandardPath; + impl StandardPath { /// Returns the metadata about the stored path. pub fn meta_header(&self) -> &PathMetaHeader { @@ -173,7 +176,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 +199,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) -> StandardPathMut { + let mut encoded_path = BytesMut::zeroed(self.encoded_path.len()); + encoded_path.copy_from_slice(self.encoded_path.as_ref()); + StandardPath { + meta_header: self.meta_header.clone(), + encoded_path, + } + } +} + +impl StandardPathMut { + /// 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/path/standard/meta_header.rs b/crates/scion-proto/src/path/standard/meta_header.rs index 2239a05..88f3f62 100644 --- a/crates/scion-proto/src/path/standard/meta_header.rs +++ b/crates/scion-proto/src/path/standard/meta_header.rs @@ -100,6 +100,33 @@ impl PathMetaHeader { self.current_hop_field.get().into() } + /// The number of interfaces on the path. + /// + /// This starts counting at 0 with the egress interface of the first AS and only counts actually + /// traversed interfaces. In particular, crossover ASes are only counted as 2 interfaces even + /// though they are represented by two hop fields. + pub fn interfaces_count(&self) -> usize { + 2 * (self.hop_fields_count() - self.info_fields_count()) + } + + /// Returns the index of the hop field including the given interface. + /// + /// This does *not* check that the `interface_index` is in range and provides meaningless + /// results if the [`Self::segment_lengths`] are invalid but does not panic in those cases. + pub fn hop_field_index_for_interface(&self, interface_index: usize) -> usize { + let actual_hop_index = (interface_index + 1) / 2; + match interface_index / 2 + 1 { + // The interface is in the first segment + x if x < self.segment_lengths[0].length() => actual_hop_index, + // The interface is in the second segment; add 1 for the additional crossover hop field + x if x + 1 < self.segment_lengths[0].length() + self.segment_lengths[1].length() => { + actual_hop_index + 1 + } + // The interface is in the third segment; add 2 for the additional crossover hop fields + _ => actual_hop_index + 2, + } + } + /// Returns the offset in bytes of the given info field. pub fn info_field_offset(info_field_index: usize) -> usize { Self::LENGTH + Self::INFO_FIELD_LENGTH * info_field_index @@ -232,3 +259,88 @@ const fn nth_field(fields: u32) -> u8 { ((fields >> ((5 - N) * FIELD_BITS)) & MASK) as u8 } + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! new_path_meta_header { + [$seg1:expr, $seg2:expr, $seg3:expr] => { + PathMetaHeader { + current_info_field: InfoFieldIndex(0), + current_hop_field: HopFieldIndex(0), + reserved: PathMetaReserved(0), + segment_lengths: [ + SegmentLength($seg1), + SegmentLength($seg2), + SegmentLength($seg3), + ], + } + }; + } + + mod interfaces_count { + use super::*; + + macro_rules! test_interfaces_count { + ($name:ident, [$seg1:expr, $seg2:expr, $seg3:expr], $count:expr) => { + #[test] + fn $name() { + assert_eq!( + new_path_meta_header![$seg1, $seg2, $seg3].interfaces_count(), + $count + ) + } + }; + } + + test_interfaces_count!(no_segment, [0, 0, 0], 0); + test_interfaces_count!(single_segment1, [2, 0, 0], 2); + test_interfaces_count!(single_segment2, [4, 0, 0], 6); + test_interfaces_count!(two_segments, [4, 3, 0], 10); + test_interfaces_count!(three_segments, [4, 3, 2], 12); + } + + mod hop_index_for_interface { + use super::*; + + macro_rules! test_hop_index_for_interface { + ($name:ident, [$seg1:expr, $seg2:expr, $seg3:expr], $interface_index:expr, $hop_index:expr) => { + #[test] + fn $name() { + assert_eq!( + new_path_meta_header![$seg1, $seg2, $seg3] + .hop_field_index_for_interface($interface_index), + $hop_index + ) + } + }; + } + test_hop_index_for_interface!(single_segment1, [4, 0, 0], 0, 0); + test_hop_index_for_interface!(single_segment2, [4, 0, 0], 1, 1); + test_hop_index_for_interface!(single_segment3, [4, 0, 0], 4, 2); + test_hop_index_for_interface!(single_segment4, [4, 0, 0], 5, 3); + test_hop_index_for_interface!(two_segments1, [4, 3, 0], 5, 3); + test_hop_index_for_interface!(two_segments2, [4, 3, 0], 6, 4); + test_hop_index_for_interface!(two_segments3, [4, 3, 0], 9, 6); + test_hop_index_for_interface!(three_segments1, [4, 3, 2], 9, 6); + test_hop_index_for_interface!(three_segments2, [4, 3, 2], 10, 7); + test_hop_index_for_interface!(three_segments3, [4, 3, 2], 11, 8); + + macro_rules! test_no_panic { + ($name:ident, [$seg1:expr, $seg2:expr, $seg3:expr]) => { + #[test] + fn $name() { + let header = new_path_meta_header![$seg1, $seg2, $seg3]; + header.hop_field_index_for_interface(0); + header.hop_field_index_for_interface(4); + } + }; + } + + test_no_panic!(no_segment, [0, 0, 0]); + test_no_panic!(invalid_segments1, [0, 0, 4]); + test_no_panic!(invalid_segments2, [2, 0, 4]); + test_no_panic!(invalid_segments3, [0, 3, 0]); + } +} diff --git a/crates/scion-proto/src/scmp.rs b/crates/scion-proto/src/scmp.rs index 7893692..81234d7 100644 --- a/crates/scion-proto/src/scmp.rs +++ b/crates/scion-proto/src/scmp.rs @@ -14,8 +14,6 @@ pub use messages::*; mod raw; pub use raw::ScmpMessageRaw; -use crate::packet::AddressHeader; - /// Trait implemented by all SCMP messages. pub trait ScmpMessageBase { /// Returns the SCMP type of this message. @@ -27,23 +25,6 @@ pub trait ScmpMessageBase { } } -/// Trait implemented by all SCMP messages to handle checksums. -pub trait ScmpMessageChecksum: ScmpMessageBase { - /// Returns the currently stored checksum of the message. - fn checksum(&self) -> u16; - - /// Clears then sets the checksum to the value returned by [`Self::calculate_checksum()`]. - fn set_checksum(&mut self, address_header: &AddressHeader); - - /// Compute the checksum for this SCMP message using the provided address header. - fn calculate_checksum(&self, address_header: &AddressHeader) -> u16; - - /// Returns true if the checksum successfully verifies, otherwise false. - fn verify_checksum(&self, address_header: &AddressHeader) -> bool { - self.calculate_checksum(address_header) == 0 - } -} - /// SCION protocol number for SCMP. /// /// See the [IETF SCION-dataplane RFC draft][rfc] for possible values. 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 6858310..6feaf5d 100644 --- a/crates/scion-proto/src/scmp/messages.rs +++ b/crates/scion-proto/src/scmp/messages.rs @@ -2,18 +2,12 @@ use bytes::{Buf, BufMut, Bytes}; -use super::{ - ScmpDecodeError, - ScmpMessageBase, - ScmpMessageChecksum, - ScmpMessageRaw, - SCMP_PROTOCOL_NUMBER, -}; +use super::{ScmpDecodeError, ScmpMessageBase, ScmpMessageRaw, SCMP_PROTOCOL_NUMBER}; use crate::{ address::IsdAsn, - packet::{AddressHeader, ChecksumDigest, InadequateBufferSize}, + packet::{AddressHeader, ChecksumDigest, InadequateBufferSize, MessageChecksum}, utils::encoded_type, - wire_encoding::WireEncodeVec, + wire_encoding::{WireDecode, WireEncodeVec}, }; /// Fully decoded SCMP message with an appropriate format. @@ -70,25 +64,31 @@ 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),*), + } + }; +} + macro_rules! lift_fn_from_scmp_variants { ( $(#[$outer:meta])* - $vis:vis fn $name:ident(&self $(,$param:ident : $param_type:ty)*) -> $return_type:ty + $vis:vis fn $name:ident(self$(: $self_ty:ty)? $(,$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),*), - } + $vis fn $name(self$(: $self_ty)? $(,$param : $param_type)*) -> $return_type { + call_method_on_scmp_variants!(self.$name($($param),*)) } }; } @@ -96,17 +96,22 @@ macro_rules! lift_fn_from_scmp_variants { impl ScmpMessage { lift_fn_from_scmp_variants!( /// Returns the type of the corresponding message. - pub fn get_type(&self) -> ScmpType + pub fn get_type(self: &Self) -> ScmpType ); lift_fn_from_scmp_variants!( /// Returns the code field of the corresponding message. - pub fn code(&self) -> u8 + pub fn code(self: &Self) -> u8 ); lift_fn_from_scmp_variants!( /// Returns true if the checksum successfully verifies, otherwise false. - pub fn verify_checksum(&self, address_header: &AddressHeader) -> bool + pub fn verify_checksum(self: &Self, address_header: &AddressHeader) -> bool + ); + + lift_fn_from_scmp_variants!( + /// Returns true if the checksum successfully verifies, otherwise false. + pub fn set_checksum(self: &mut Self, address_header: &AddressHeader) -> () ); /// Returns true iff `self` is an error message. @@ -157,19 +162,27 @@ 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; lift_fn_from_scmp_variants!( - fn encode_with_unchecked(&self, buffer: &mut bytes::BytesMut) -> [Bytes; 2] + fn encode_with_unchecked(self: &Self, buffer: &mut bytes::BytesMut) -> [Bytes; 2] ); - lift_fn_from_scmp_variants!(fn total_length(&self) -> usize); + lift_fn_from_scmp_variants!(fn total_length(self: &Self) -> usize); - lift_fn_from_scmp_variants!(fn required_capacity(&self) -> usize); + lift_fn_from_scmp_variants!(fn required_capacity(self: &Self) -> usize); } -trait ScmpMessageEncodeDecode: ScmpMessageBase + ScmpMessageChecksum + Sized { +trait ScmpMessageEncodeDecode: ScmpMessageBase + MessageChecksum + Sized { const INFO_BLOCK_LENGTH: usize; #[allow(unused_variables)] @@ -221,7 +234,7 @@ impl WireEncodeVec<2> for T { } /// Trait implemented by all SCMP error messages. -pub trait ScmpErrorMessage: ScmpMessageBase + ScmpMessageChecksum { +pub trait ScmpErrorMessage: ScmpMessageBase + MessageChecksum { /// Get the (truncated) packet that triggered the error. fn get_offending_packet(&self) -> Bytes; } @@ -248,6 +261,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) @@ -266,8 +291,7 @@ macro_rules! impl_conversion_and_type { )? } - impl ScmpMessageChecksum for $name { - + impl MessageChecksum for $name { fn checksum(&self) -> u16 { self.checksum } @@ -599,7 +623,7 @@ impl ScmpMessageEncodeDecode for ScmpInternalConnectivityDown { } /// Trait implemented by all SCMP informational messages. -pub trait ScmpInformationalMessage: ScmpMessageBase + ScmpMessageChecksum { +pub trait ScmpInformationalMessage: ScmpMessageBase + MessageChecksum { /// Get the message's identifier. fn get_identifier(&self) -> u16; /// Get the message's sequence number. diff --git a/crates/scion-proto/src/scmp/raw.rs b/crates/scion-proto/src/scmp/raw.rs index 7f96049..fd3598a 100644 --- a/crates/scion-proto/src/scmp/raw.rs +++ b/crates/scion-proto/src/scmp/raw.rs @@ -2,15 +2,9 @@ use bytes::{Buf, BufMut, Bytes}; -use super::{ - ScmpDecodeError, - ScmpMessageBase, - ScmpMessageChecksum, - ScmpType, - SCMP_PROTOCOL_NUMBER, -}; +use super::{ScmpDecodeError, ScmpMessageBase, ScmpType, SCMP_PROTOCOL_NUMBER}; use crate::{ - packet::{AddressHeader, ChecksumDigest, InadequateBufferSize}, + packet::{AddressHeader, ChecksumDigest, InadequateBufferSize, MessageChecksum}, wire_encoding::{WireDecode, WireEncodeVec}, }; @@ -53,7 +47,7 @@ impl ScmpMessageBase for ScmpMessageRaw { } } -impl ScmpMessageChecksum for ScmpMessageRaw { +impl MessageChecksum for ScmpMessageRaw { fn checksum(&self) -> u16 { self.checksum } diff --git a/crates/scion/src/udp_socket.rs b/crates/scion/src/udp_socket.rs index d3dd03e..8d5f220 100644 --- a/crates/scion/src/udp_socket.rs +++ b/crates/scion/src/udp_socket.rs @@ -10,7 +10,7 @@ use chrono::Utc; use scion_proto::{ address::SocketAddr, datagram::{UdpEncodeError, UdpMessage}, - packet::{ByEndpoint, ScionPacketRaw, ScionPacketUdp}, + packet::{ByEndpoint, MessageChecksum, ScionPacketRaw, ScionPacketUdp}, path::{DataplanePath, Path}, reliable::Packet, wire_encoding::WireDecode, diff --git a/crates/scion/tests/test_scmp_packet.rs b/crates/scion/tests/test_scmp_packet.rs new file mode 100644 index 0000000..e48ff24 --- /dev/null +++ b/crates/scion/tests/test_scmp_packet.rs @@ -0,0 +1,60 @@ +use std::time::Duration; + +use scion::{ + daemon::{get_daemon_address, DaemonClient}, + dispatcher::{get_dispatcher_path, DispatcherStream}, +}; +use scion_proto::{ + address::{ScionAddr, SocketAddr}, + packet::{ByEndpoint, ScionPacketScmp}, + scmp::{ScmpInformationalMessage, ScmpMessage, ScmpTracerouteRequest}, + wire_encoding::WireDecode, +}; + +type TestResult = Result>; +const TIMEOUT: Duration = std::time::Duration::from_secs(1); + +#[tokio::test] +#[ignore = "requires daemon and dispatcher"] +async fn scmp_traceroute() -> TestResult { + let endpoints: ByEndpoint = ByEndpoint { + source: "1-ff00:0:111,127.0.0.17".parse().unwrap(), + destination: "1-ff00:0:112,::1".parse().unwrap(), + }; + let daemon_client = DaemonClient::connect(&get_daemon_address()) + .await + .expect("should be able to connect"); + let path_forward = daemon_client + .paths_to(endpoints.destination.isd_asn()) + .await? + .next() + .unwrap(); + println!("Forward path: {:?}", path_forward.dataplane_path); + + let mut stream = DispatcherStream::connect(get_dispatcher_path()).await?; + let _local_address = stream + .register(SocketAddr::new(endpoints.source, 0)) + .await?; + + let request = ScionPacketScmp::new_traceroute_request(&endpoints, &path_forward, 1, 1, 3)?; + + stream + .send_packet_via(path_forward.underlay_next_hop, request.clone()) + .await?; + + let mut packet = tokio::time::timeout(TIMEOUT, stream.receive_packet()).await??; + let scion_packet_scmp = ScionPacketScmp::decode(&mut packet.content)?; + let scmp_message = scion_packet_scmp.message; + assert!(scmp_message.verify_checksum(&scion_packet_scmp.headers.address)); + + let ScmpMessage::TracerouteReply(reply) = scmp_message else { + panic!("Unexpected SCMP reply received.") + }; + + assert_eq!( + reply.get_message_id(), + ScmpTracerouteRequest::try_from(request.message)?.get_message_id() + ); + + Ok(()) +}