Skip to main content

scion_stack/scionstack/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 scion_proto::{
18    packet::{ByEndpoint, ScionPacketRaw, ScionPacketScmp},
19    scmp::{ScmpEchoReply, ScmpMessage},
20};
21
22use super::ScmpHandler;
23
24/// Handler to reply to echo requests. The handler only makes sense for sockets that are bound
25/// to the default SCION port 30041 <https://docs.scion.org/en/latest/dev/design/router-port-dispatch.html#scmp>
26pub struct DefaultEchoHandler;
27
28impl Default for DefaultEchoHandler {
29    fn default() -> Self {
30        Self
31    }
32}
33
34impl DefaultEchoHandler {
35    /// Create a new default echo handler.
36    pub fn new() -> Self {
37        Self
38    }
39
40    fn try_echo_reply(&self, p_raw: ScionPacketRaw) -> anyhow::Result<Option<ScionPacketScmp>> {
41        let p: ScionPacketScmp = p_raw.try_into().context("Failed to decode packet")?;
42        let reply_msg = match p.message {
43            ScmpMessage::EchoRequest(r) => {
44                tracing::debug!("Echo request received, sending echo reply");
45                ScmpMessage::EchoReply(ScmpEchoReply::new(r.identifier, r.sequence_number, r.data))
46            }
47            _ => return Ok(None),
48        };
49        let reply_path = p
50            .headers
51            .reversed_path(None)
52            .context("Failed to reverse SCMP echo path")?
53            .data_plane_path;
54
55        let src = p
56            .headers
57            .address
58            .source()
59            .context("Failed to decode source address")?;
60
61        let dst = p
62            .headers
63            .address
64            .destination()
65            .context("Failed to decode destination address")?;
66
67        let reply = ScionPacketScmp::new(
68            ByEndpoint {
69                source: dst,
70                destination: src,
71            },
72            reply_path,
73            reply_msg,
74        )
75        .context("Failed to encode reply")?;
76        Ok(Some(reply))
77    }
78}
79
80impl ScmpHandler for DefaultEchoHandler {
81    fn handle(&self, p_raw: ScionPacketRaw) -> Option<ScionPacketRaw> {
82        match self.try_echo_reply(p_raw) {
83            Ok(Some(reply)) => {
84                tracing::debug!(
85                    src = ?reply.headers.address.source(),
86                    dst = ?reply.headers.address.destination(),
87                    "Sending echo reply"
88                );
89                Some(reply.into())
90            }
91            Ok(None) => None,
92            Err(e) => {
93                tracing::info!(error = %e, "Received invalid SCMP echo request");
94                None
95            }
96        }
97    }
98}
99
100#[cfg(test)]
101mod default_echo_handler_tests {
102    use bytes::Bytes;
103    use scion_proto::{
104        address::{Asn, EndhostAddr, Isd, IsdAsn},
105        path::test_builder::{TestPathBuilder, TestPathContext},
106        scmp::{ScmpEchoReply, ScmpEchoRequest, ScmpMessage},
107    };
108
109    use super::*;
110
111    fn test_context() -> TestPathContext {
112        let src = EndhostAddr::new(IsdAsn::new(Isd(1), Asn(10)), [192, 0, 2, 1].into());
113        let dst = EndhostAddr::new(IsdAsn::new(Isd(1), Asn(20)), [198, 51, 100, 1].into());
114        TestPathBuilder::new(src, dst)
115            .using_info_timestamp(42)
116            .up()
117            .add_hop(0, 11)
118            .add_hop(12, 0)
119            .build(77)
120    }
121
122    #[test]
123    fn replies_to_echo_request() {
124        let ctx = test_context();
125        let expected_src = ctx.dst_address.into();
126        let expected_dst = ctx.src_address.into();
127        let request = ctx.scion_packet_scmp(ScmpMessage::EchoRequest(ScmpEchoRequest::new(
128            7,
129            9,
130            Bytes::from_static(b"payload"),
131        )));
132
133        let handler = DefaultEchoHandler::new();
134        let reply = handler.handle(request.into());
135        assert!(reply.is_some());
136        let reply = reply.unwrap();
137        let reply: ScionPacketScmp = reply.try_into().expect("valid SCMP packet in returning");
138        match reply.message {
139            ScmpMessage::EchoReply(r) => {
140                assert_eq!(r.get_identifier(), 7);
141                assert_eq!(r.get_sequence_number(), 9);
142                assert_eq!(r.data, Bytes::from_static(b"payload"));
143            }
144            other => panic!("unexpected reply message: {other:?}"),
145        }
146        assert_eq!(reply.headers.address.source().unwrap(), expected_src);
147        assert_eq!(reply.headers.address.destination().unwrap(), expected_dst);
148    }
149
150    #[test]
151    fn ignores_non_echo_messages() {
152        let ctx = test_context();
153        let handler = DefaultEchoHandler::new();
154
155        let non_echo = ctx.scion_packet_scmp(ScmpMessage::EchoReply(ScmpEchoReply::new(
156            1,
157            2,
158            Bytes::from_static(b"resp"),
159        )));
160
161        let reply = handler.handle(non_echo.into());
162        assert!(reply.is_none());
163    }
164
165    #[test]
166    fn ignores_packets_that_fail_decoding() {
167        let ctx = test_context();
168        let handler = DefaultEchoHandler::new();
169
170        let wrong_protocol = ctx.scion_packet_raw(b"not scmp");
171        let reply = handler.handle(wrong_protocol);
172        assert!(reply.is_none());
173    }
174}