Skip to main content

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