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