use anyhow::Context as _;
use sciparse::{
dataplane_path::view::ScionDpPathViewExt,
packet::{
model::{ScionRawPacket, ScionScmpPacket},
view::ScionRawPacketView,
},
payload::scmp::{
model::{ScmpEchoReply, ScmpMessage},
view::ScmpMessageView,
},
};
use super::ScmpHandler;
pub struct DefaultEchoHandler;
impl Default for DefaultEchoHandler {
fn default() -> Self {
Self
}
}
impl DefaultEchoHandler {
pub fn new() -> Self {
Self
}
fn try_echo_reply(
&self,
p_raw: &ScionRawPacketView,
) -> anyhow::Result<Option<ScionScmpPacket>> {
let p = p_raw
.try_as_scmp()
.context("Packet is not a valid SCMP packet")?;
let reply_msg = match p.scmp().message() {
ScmpMessageView::EchoRequest(r) => {
tracing::debug!("Echo request received, sending echo reply");
ScmpMessage::EchoReply(ScmpEchoReply::new(
r.identifier(),
r.sequence_number(),
r.data().to_vec(),
))
}
_ => return Ok(None),
};
let reply_path = p
.header()
.path()
.to_model()
.try_into_reversed()
.map_err(|(_, e)| anyhow::anyhow!("Failed to reverse path: {e:?}"))?;
let src = p
.src_scion_addr()
.context("Failed to decode source address")?;
let dst = p
.dst_scion_addr()
.context("Failed to decode destination address")?;
let reply = ScionScmpPacket::new(dst, src, reply_path, reply_msg);
Ok(Some(reply))
}
}
impl ScmpHandler for DefaultEchoHandler {
fn handle(&self, p_raw: &ScionRawPacketView) -> Option<ScionRawPacket> {
match self.try_echo_reply(p_raw) {
Ok(Some(reply)) => {
tracing::debug!(
src = ?reply.src_scion_addr(),
dst = ?reply.dst_scion_addr(),
"Sending echo reply"
);
Some(reply.into())
}
Ok(None) => None,
Err(e) => {
tracing::info!(error = %e, "Received invalid SCMP echo request");
None
}
}
}
}
#[cfg(test)]
mod default_echo_handler_tests {
use sciparse::{
address::ip_addr::ScionIpAddr,
core::model::Model,
identifier::{asn::Asn, isd::Isd, isd_asn::IsdAsn},
payload::scmp::model::ScmpEchoRequest,
util::test_builder::{TestPathBuilder, TestPathContext},
};
use super::*;
fn test_context() -> TestPathContext {
let src = ScionIpAddr::new(IsdAsn::new(Isd(1), Asn(10)), [192, 0, 2, 1].into());
let dst = ScionIpAddr::new(IsdAsn::new(Isd(1), Asn(20)), [198, 51, 100, 1].into());
TestPathBuilder::new(src.into(), dst.into())
.using_info_timestamp(42)
.up()
.add_hop(0, 11)
.add_hop(12, 0)
.build(77)
}
#[test]
fn replies_to_echo_request() {
let ctx = test_context();
let expected_src = ctx.dst_address;
let expected_dst = ctx.src_address;
let request = ctx
.scion_packet_scmp(ScmpEchoRequest::new(7, 9, b"payload".to_vec()).into())
.into_raw()
.try_encode_to_owned_view()
.expect("should encode");
let handler = DefaultEchoHandler::new();
let reply = handler.handle(&request);
assert!(reply.is_some());
let reply = reply.unwrap();
let reply = reply
.try_into_scmp()
.expect("valid SCMP packet in returning");
match &reply.payload {
ScmpMessage::EchoReply(r) => {
assert_eq!(r.identifier, 7);
assert_eq!(r.sequence_number, 9);
assert_eq!(r.data, b"payload");
}
other => panic!("unexpected reply message: {other:?}"),
}
assert_eq!(reply.src_scion_addr().unwrap(), expected_src);
assert_eq!(reply.dst_scion_addr().unwrap(), expected_dst);
}
#[test]
fn ignores_non_echo_messages() {
let ctx = test_context();
let handler = DefaultEchoHandler::new();
let non_echo = ctx
.scion_packet_scmp(ScmpEchoReply::new(1, 2, b"resp".to_vec()).into())
.into_raw()
.try_encode_to_owned_view()
.expect("should encode");
let reply = handler.handle(&non_echo);
assert!(reply.is_none());
}
#[test]
fn ignores_packets_that_fail_decoding() {
let ctx = test_context();
let handler = DefaultEchoHandler::new();
let wrong_protocol = ctx
.scion_packet_raw(b"not scmp")
.try_encode_to_owned_view()
.expect("should encode");
let reply = handler.handle(&wrong_protocol);
assert!(reply.is_none());
}
}