Skip to main content

scion_stack/scionstack/scmp_handler/
error.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//! SCMP error handling implementation.
15
16use scion_proto::{
17    packet::{ScionPacketRaw, ScionPacketScmp},
18    scmp::ScmpErrorMessage,
19};
20
21use super::ScmpHandler;
22use crate::{scionstack::scmp_handler::ScmpErrorReceiver, types::Subscribers};
23
24/// A SCMP handler that forwards SCMP messages to SCMP error receivers.
25pub struct ScmpErrorHandler {
26    receivers: Subscribers<dyn ScmpErrorReceiver>,
27}
28
29impl ScmpErrorHandler {
30    /// Creates a new forwarding SCMP handler.
31    pub fn new(receivers: Subscribers<dyn ScmpErrorReceiver>) -> Self {
32        Self { receivers }
33    }
34}
35
36impl ScmpHandler for ScmpErrorHandler {
37    fn handle(&self, pkt: ScionPacketRaw) -> Option<ScionPacketRaw> {
38        let path = pkt.headers.path();
39        let scmp_pkg: ScionPacketScmp = if let Ok(scmp_pkg) = pkt.try_into() {
40            scmp_pkg
41        } else {
42            return None;
43        };
44
45        let scmp_error: ScmpErrorMessage = match scmp_pkg.message.try_into() {
46            Ok(scmp_error) => scmp_error,
47            Err(_) => {
48                tracing::debug!("ignoring non error SCMP message");
49                return None;
50            }
51        };
52
53        tracing::debug!(err = ?scmp_error, "reporting SCMP error");
54        self.receivers.for_each(|receiver| {
55            receiver.report_scmp_error(scmp_error.clone(), &path);
56        });
57        None
58    }
59}
60
61#[cfg(test)]
62mod scmp_error_handler_tests {
63    use std::sync::Arc;
64
65    use bytes::Bytes;
66    use scion_proto::{
67        address::{Asn, EndhostAddr, Isd, IsdAsn},
68        path::{
69            Path,
70            test_builder::{TestPathBuilder, TestPathContext},
71        },
72        scmp::{
73            DestinationUnreachableCode, ScmpDestinationUnreachable, ScmpEchoReply, ScmpEchoRequest,
74            ScmpMessage,
75        },
76    };
77
78    use super::*;
79
80    fn test_context() -> TestPathContext {
81        let src = EndhostAddr::new(IsdAsn::new(Isd(1), Asn(10)), [192, 0, 2, 1].into());
82        let dst = EndhostAddr::new(IsdAsn::new(Isd(1), Asn(20)), [198, 51, 100, 1].into());
83        TestPathBuilder::new(src, dst)
84            .using_info_timestamp(42)
85            .up()
86            .add_hop(0, 11)
87            .add_hop(12, 0)
88            .build(77)
89    }
90
91    #[test]
92    fn forwards_scmp_error_messages_to_receivers() {
93        let ctx = test_context();
94        let scmp_msg = ScmpMessage::DestinationUnreachable(ScmpDestinationUnreachable::new(
95            DestinationUnreachableCode::AddressUnreachable,
96            Bytes::from_static(b"offending packet"),
97        ));
98        let packet = ctx.scion_packet_scmp(scmp_msg);
99        let expected_path = packet.headers.path();
100
101        let mut mock_receiver = crate::scionstack::scmp_handler::MockScmpErrorReceiver::new();
102        mock_receiver
103            .expect_report_scmp_error()
104            .withf(move |error: &ScmpErrorMessage, p: &Path| {
105                matches!(error, ScmpErrorMessage::DestinationUnreachable(_)) && p == &expected_path
106            })
107            .times(1)
108            .returning(|_, _| {});
109
110        let receiver_arc: Arc<dyn ScmpErrorReceiver> = Arc::new(mock_receiver);
111        let subscribers = Subscribers::new();
112        subscribers.register(receiver_arc.clone());
113
114        let handler = ScmpErrorHandler::new(subscribers);
115        let result = handler.handle(packet.into());
116
117        assert!(result.is_none());
118        drop(receiver_arc); // ensure mock lives until assertions complete
119    }
120
121    #[test]
122    fn ignores_non_error_scmp_messages() {
123        let ctx = test_context();
124        let mut mock_receiver = crate::scionstack::scmp_handler::MockScmpErrorReceiver::new();
125        mock_receiver.expect_report_scmp_error().times(0);
126
127        let receiver_arc: Arc<dyn ScmpErrorReceiver> = Arc::new(mock_receiver);
128        let subscribers = Subscribers::new();
129        subscribers.register(receiver_arc.clone());
130
131        let handler = ScmpErrorHandler::new(subscribers);
132
133        // Test with EchoRequest
134        let echo_request = ctx.scion_packet_scmp(ScmpMessage::EchoRequest(ScmpEchoRequest::new(
135            1,
136            2,
137            Bytes::from_static(b"data"),
138        )));
139        let result = handler.handle(echo_request.into());
140        assert!(result.is_none());
141
142        // Test with EchoReply
143        let echo_reply = ctx.scion_packet_scmp(ScmpMessage::EchoReply(ScmpEchoReply::new(
144            1,
145            2,
146            Bytes::from_static(b"data"),
147        )));
148        let result = handler.handle(echo_reply.into());
149        assert!(result.is_none());
150        drop(receiver_arc);
151    }
152
153    #[test]
154    fn ignores_invalid_packets() {
155        let ctx = test_context();
156        let mut mock_receiver = crate::scionstack::scmp_handler::MockScmpErrorReceiver::new();
157        mock_receiver.expect_report_scmp_error().times(0);
158
159        let receiver_arc: Arc<dyn ScmpErrorReceiver> = Arc::new(mock_receiver);
160        let subscribers = Subscribers::new();
161        subscribers.register(receiver_arc.clone());
162
163        let handler = ScmpErrorHandler::new(subscribers);
164
165        // Test with invalid packet data
166        let invalid_packet = ctx.scion_packet_raw(b"not scmp");
167        let result = handler.handle(invalid_packet);
168        assert!(result.is_none());
169        drop(receiver_arc);
170    }
171
172    #[test]
173    fn handles_multiple_receivers() {
174        let ctx = test_context();
175        let error_msg = ScmpMessage::DestinationUnreachable(ScmpDestinationUnreachable::new(
176            DestinationUnreachableCode::AddressUnreachable,
177            Bytes::from_static(b"offending packet"),
178        ));
179        let packet = ctx.scion_packet_scmp(error_msg);
180        let expected_path = packet.headers.path();
181
182        let expected_path_clone1 = expected_path.clone();
183        let expected_path_clone2 = expected_path.clone();
184        let mut mock_receiver1 = crate::scionstack::scmp_handler::MockScmpErrorReceiver::new();
185        mock_receiver1
186            .expect_report_scmp_error()
187            .withf(move |error: &ScmpErrorMessage, p: &Path| {
188                matches!(error, ScmpErrorMessage::DestinationUnreachable(_))
189                    && p == &expected_path_clone1
190            })
191            .times(1)
192            .returning(|_, _| {});
193
194        let mut mock_receiver2 = crate::scionstack::scmp_handler::MockScmpErrorReceiver::new();
195        mock_receiver2
196            .expect_report_scmp_error()
197            .withf(move |error: &ScmpErrorMessage, p: &Path| {
198                matches!(error, ScmpErrorMessage::DestinationUnreachable(_))
199                    && p == &expected_path_clone2
200            })
201            .times(1)
202            .returning(|_, _| {});
203
204        let receiver1_arc: Arc<dyn ScmpErrorReceiver> = Arc::new(mock_receiver1);
205        let receiver2_arc: Arc<dyn ScmpErrorReceiver> = Arc::new(mock_receiver2);
206        let subscribers = Subscribers::new();
207        subscribers.register(receiver1_arc.clone());
208        subscribers.register(receiver2_arc.clone());
209
210        let handler = ScmpErrorHandler::new(subscribers);
211        let result = handler.handle(packet.into());
212
213        assert!(result.is_none());
214        drop(receiver1_arc);
215        drop(receiver2_arc);
216    }
217
218    #[test]
219    fn handles_weak_references() {
220        let ctx = test_context();
221        let error_msg = ScmpMessage::DestinationUnreachable(ScmpDestinationUnreachable::new(
222            DestinationUnreachableCode::AddressUnreachable,
223            Bytes::from_static(b"offending packet"),
224        ));
225        let packet = ctx.scion_packet_scmp(error_msg);
226
227        let mut mock_receiver = crate::scionstack::scmp_handler::MockScmpErrorReceiver::new();
228        // When the strong reference is dropped, the weak reference won't upgrade,
229        // so report_scmp_error should not be called
230        mock_receiver.expect_report_scmp_error().times(0);
231
232        let receiver_arc: Arc<dyn ScmpErrorReceiver> = Arc::new(mock_receiver);
233        let subscribers = Subscribers::new();
234        subscribers.register(receiver_arc);
235
236        // The Arc was moved into register, so the weak reference should not upgrade
237
238        let handler = ScmpErrorHandler::new(subscribers);
239        let result = handler.handle(packet.into());
240
241        // Handler should return None even when weak references fail to upgrade
242        assert!(result.is_none());
243    }
244}