Skip to main content

scion_stack/stack/scmp_handler/
echo.rs

1// Copyright 2026 Anapaya Systems
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Default SCMP echo handler.
15
16use anyhow::Context as _;
17use sciparse::{
18    dataplane_path::view::ScionDpPathViewExt,
19    packet::{
20        model::{ScionRawPacket, ScionScmpPacket},
21        view::ScionRawPacketView,
22    },
23    payload::scmp::{
24        model::{ScmpEchoReply, ScmpMessage},
25        view::ScmpMessageView,
26    },
27};
28
29use super::ScmpHandler;
30
31/// Handler to reply to echo requests. The handler only makes sense for sockets that are bound
32/// to the default SCION port 30041 <https://docs.scion.org/en/latest/dev/design/router-port-dispatch.html#scmp>
33pub struct DefaultEchoHandler;
34
35impl Default for DefaultEchoHandler {
36    fn default() -> Self {
37        Self
38    }
39}
40
41impl DefaultEchoHandler {
42    /// Create a new default echo handler.
43    pub fn new() -> Self {
44        Self
45    }
46
47    fn try_echo_reply(
48        &self,
49        p_raw: &ScionRawPacketView,
50    ) -> anyhow::Result<Option<ScionScmpPacket>> {
51        let p = p_raw
52            .try_as_scmp()
53            .context("Packet is not a valid SCMP packet")?;
54
55        let reply_msg = match p.scmp().message() {
56            ScmpMessageView::EchoRequest(r) => {
57                tracing::debug!("Echo request received, sending echo reply");
58                ScmpMessage::EchoReply(ScmpEchoReply::new(
59                    r.identifier(),
60                    r.sequence_number(),
61                    r.data().to_vec(),
62                ))
63            }
64            _ => return Ok(None),
65        };
66        let reply_path = p
67            .header()
68            .path()
69            .to_model()
70            .try_into_reversed()
71            .map_err(|(_, e)| anyhow::anyhow!("Failed to reverse path: {e:?}"))?;
72
73        let src = p
74            .src_scion_addr()
75            .context("Failed to decode source address")?;
76
77        let dst = p
78            .dst_scion_addr()
79            .context("Failed to decode destination address")?;
80
81        let reply = ScionScmpPacket::new(dst, src, reply_path, reply_msg);
82
83        Ok(Some(reply))
84    }
85}
86
87impl ScmpHandler for DefaultEchoHandler {
88    fn handle(&self, p_raw: &ScionRawPacketView) -> Option<ScionRawPacket> {
89        match self.try_echo_reply(p_raw) {
90            Ok(Some(reply)) => {
91                tracing::debug!(
92                    src = ?reply.src_scion_addr(),
93                    dst = ?reply.dst_scion_addr(),
94                    "Sending echo reply"
95                );
96                Some(reply.into())
97            }
98            Ok(None) => None,
99            Err(e) => {
100                tracing::info!(error = %e, "Received invalid SCMP echo request");
101                None
102            }
103        }
104    }
105}
106
107#[cfg(test)]
108mod default_echo_handler_tests {
109    use sciparse::{
110        address::ip_addr::ScionIpAddr,
111        core::model::Model,
112        identifier::{asn::Asn, isd::Isd, isd_asn::IsdAsn},
113        payload::scmp::model::ScmpEchoRequest,
114        util::test_builder::{TestPathBuilder, TestPathContext},
115    };
116
117    use super::*;
118
119    fn test_context() -> TestPathContext {
120        let src = ScionIpAddr::new(IsdAsn::new(Isd(1), Asn(10)), [192, 0, 2, 1].into());
121        let dst = ScionIpAddr::new(IsdAsn::new(Isd(1), Asn(20)), [198, 51, 100, 1].into());
122        TestPathBuilder::new(src.into(), dst.into())
123            .using_info_timestamp(42)
124            .up()
125            .add_hop(0, 11)
126            .add_hop(12, 0)
127            .build(77)
128    }
129
130    #[test]
131    fn replies_to_echo_request() {
132        let ctx = test_context();
133        let expected_src = ctx.dst_address;
134        let expected_dst = ctx.src_address;
135        let request = ctx
136            .scion_packet_scmp(ScmpEchoRequest::new(7, 9, b"payload".to_vec()).into())
137            .into_raw()
138            .try_encode_to_owned_view()
139            .expect("should encode");
140
141        let handler = DefaultEchoHandler::new();
142        let reply = handler.handle(&request);
143        assert!(reply.is_some());
144        let reply = reply.unwrap();
145        let reply = reply
146            .try_into_scmp()
147            .expect("valid SCMP packet in returning");
148        match &reply.payload {
149            ScmpMessage::EchoReply(r) => {
150                assert_eq!(r.identifier, 7);
151                assert_eq!(r.sequence_number, 9);
152                assert_eq!(r.data, b"payload");
153            }
154            other => panic!("unexpected reply message: {other:?}"),
155        }
156        assert_eq!(reply.src_scion_addr().unwrap(), expected_src);
157        assert_eq!(reply.dst_scion_addr().unwrap(), expected_dst);
158    }
159
160    #[test]
161    fn ignores_non_echo_messages() {
162        let ctx = test_context();
163        let handler = DefaultEchoHandler::new();
164
165        let non_echo = ctx
166            .scion_packet_scmp(ScmpEchoReply::new(1, 2, b"resp".to_vec()).into())
167            .into_raw()
168            .try_encode_to_owned_view()
169            .expect("should encode");
170
171        let reply = handler.handle(&non_echo);
172        assert!(reply.is_none());
173    }
174
175    #[test]
176    fn ignores_packets_that_fail_decoding() {
177        let ctx = test_context();
178        let handler = DefaultEchoHandler::new();
179
180        let wrong_protocol = ctx
181            .scion_packet_raw(b"not scmp")
182            .try_encode_to_owned_view()
183            .expect("should encode");
184        let reply = handler.handle(&wrong_protocol);
185        assert!(reply.is_none());
186    }
187}