Skip to main content

scion_stack/underlays/
udp.rs

1// Copyright 2025 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//! UDP underlay socket.
15use std::{
16    io,
17    net::{self},
18    sync::Arc,
19};
20
21use async_trait::async_trait;
22use sciparse::{
23    address::ip_socket_addr::ScionSocketIpAddr,
24    core::view::View,
25    dataplane_path::view::ScionDpPathViewExt,
26    identifier::isd_asn::IsdAsn,
27    packet::view::{ScionPacketView, ScionRawPacketView},
28    path::metadata::path_interface::PathInterface,
29};
30use tokio::net::UdpSocket;
31
32use crate::{
33    stack::{ScionSocketReceiveError, ScionSocketSendError, UnderlaySocket},
34    underlays::{discovery::UnderlayDiscovery, outbound_ip_towards},
35};
36
37/// A trait for resolving the local IP addresses for outbound connections.
38#[async_trait::async_trait]
39pub trait OutboundIpResolver: Send + Sync {
40    /// Returns the local IP addresses of the host that can be used to reach the network.
41    async fn outbound_ips(&self) -> Vec<net::IpAddr>;
42}
43
44#[async_trait::async_trait]
45impl OutboundIpResolver for Vec<net::IpAddr> {
46    async fn outbound_ips(&self) -> Vec<net::IpAddr> {
47        self.clone()
48    }
49}
50
51/// An outbound IP resolver that resolves the local IP address that can be used to reach a target
52/// API address.
53///
54/// DNS overrides in `host_overrides` take precedence over the OS resolver. This allows
55/// the resolver to be used in environments where the OS resolver is not aware of custom
56/// hostname-to-IP mappings (e.g. when `ConnectParameters::hosts` is set).
57pub struct TargetAddrOutboundIpResolver {
58    url: url::Url,
59    host_overrides: Vec<(String, net::IpAddr)>,
60}
61
62impl TargetAddrOutboundIpResolver {
63    /// Create an outbound IP resolver for the given target `url` with a set of hostname-to-IP
64    /// overrides.
65    ///
66    /// When resolving the outbound IP towards `url`, entries in `host_overrides` are checked before
67    /// the OS resolver. Pass an empty vec when no overrides are needed.
68    pub fn new(url: url::Url, host_overrides: Vec<(String, net::IpAddr)>) -> Self {
69        Self {
70            url,
71            host_overrides,
72        }
73    }
74}
75
76#[async_trait::async_trait]
77impl OutboundIpResolver for TargetAddrOutboundIpResolver {
78    /// Returns the local IP addresses that can reach the target URL.
79    ///
80    /// Resolution order:
81    /// 1. `host_overrides` map
82    /// 2. Literal IP in the URL
83    /// 3. OS DNS resolver
84    async fn outbound_ips(&self) -> Vec<net::IpAddr> {
85        let url = &self.url;
86        let host = match url.host_str() {
87            Some(h) => h,
88            None => return vec![],
89        };
90        let port = url.port_or_known_default().unwrap_or(0);
91
92        let target = if let Some((_, ip)) = self.host_overrides.iter().find(|(h, _)| h == host) {
93            net::SocketAddr::new(*ip, port)
94        } else if let Ok(ip) = host.parse::<net::IpAddr>() {
95            net::SocketAddr::new(ip, port)
96        } else {
97            match url
98                .socket_addrs(|| None)
99                .ok()
100                .and_then(|addrs| addrs.into_iter().next())
101            {
102                Some(addr) => addr,
103                None => {
104                    tracing::warn!(url = %url, "failed to resolve API URL for local IP detection");
105                    return vec![];
106                }
107            }
108        };
109
110        match outbound_ip_towards(target).await {
111            Some(ip) => vec![ip],
112            None => vec![],
113        }
114    }
115}
116
117/// A UDP underlay socket.
118pub(crate) struct UdpUnderlaySocket {
119    pub(crate) socket: UdpSocket,
120    pub(crate) bind_addr: ScionSocketIpAddr,
121    pub(crate) underlay_discovery: Arc<dyn UnderlayDiscovery>,
122}
123
124impl UdpUnderlaySocket {
125    pub(crate) fn new(
126        socket: UdpSocket,
127        bind_addr: ScionSocketIpAddr,
128        underlay_discovery: Arc<dyn UnderlayDiscovery>,
129    ) -> Self {
130        Self {
131            socket,
132            bind_addr,
133            underlay_discovery,
134        }
135    }
136
137    /// Dispatch a packet to the local AS network.
138    fn resolve_local_dispatch_addr(
139        &self,
140        packet: &ScionPacketView,
141    ) -> Result<net::SocketAddr, ScionSocketSendError> {
142        let dst_addr = packet
143            .header()
144            .dst_host_addr()
145            .map_err(|e| {
146                crate::stack::ScionSocketSendError::InvalidPacket(
147                    format!("Packet for local dispatch had invalid destination address: {e}")
148                        .into(),
149                )
150            })?
151            .ip()
152            .ok_or(crate::stack::ScionSocketSendError::InvalidPacket(
153                "Packet for local dispatch dst was not an IP address".into(),
154            ))?;
155
156        let classified = packet.try_classify().map_err(|e| {
157            crate::stack::ScionSocketSendError::InvalidPacket(
158                format!("Failed to classify packet for local dispatch: {e}").into(),
159            )
160        })?;
161
162        let dst_port =
163            classified
164                .dst_port()
165                .ok_or(crate::stack::ScionSocketSendError::InvalidPacket(
166                    "Coulldn't determine destination port in packet for local dispatch".into(),
167                ))?;
168
169        Ok(net::SocketAddr::new(dst_addr, dst_port))
170    }
171
172    fn try_dispatch_local(&self, packet: &ScionPacketView) -> Result<(), ScionSocketSendError> {
173        let dst_addr = self.resolve_local_dispatch_addr(packet)?;
174        self.socket
175            .try_send_to(packet.as_slice(), dst_addr)
176            .map_err(|e| Self::map_send_io_error(e, packet.header().src_ia(), 0, dst_addr))?;
177        Ok(())
178    }
179
180    /// Map a UDP send error to a SCION socket send error.
181    fn map_send_io_error(
182        e: io::Error,
183        src: IsdAsn,
184        interface_id: u16,
185        next_hop: net::SocketAddr,
186    ) -> ScionSocketSendError {
187        use std::io::ErrorKind::{
188            BrokenPipe, ConnectionAborted, ConnectionReset, HostUnreachable, NetworkUnreachable,
189        };
190        match e.kind() {
191            HostUnreachable | NetworkUnreachable => {
192                ScionSocketSendError::UnderlayNextHopUnreachable {
193                    isd_as: src,
194                    interface_id,
195                    address: Some(next_hop),
196                    msg: e.to_string(),
197                }
198            }
199            ConnectionAborted | ConnectionReset | BrokenPipe => ScionSocketSendError::Closed,
200            _ => ScionSocketSendError::IoError(e),
201        }
202    }
203}
204
205#[async_trait]
206impl UnderlaySocket for UdpUnderlaySocket {
207    /// Try to send a raw packet immediately. Takes a `ScionPacketRaw` because it needs to read the
208    /// path to resolve the underlay next hop.
209    fn try_send(&self, packet: &ScionRawPacketView) -> Result<(), ScionSocketSendError> {
210        let source_ia = packet.header().src_ia();
211
212        // If packet is destined for the local AS, dispatch it locally.
213        if packet.header().dst_ia() == source_ia {
214            return self.try_dispatch_local(packet);
215        }
216
217        // Get the current egress interface from the path
218        let Some(egress_if) = packet.header().path().first_egress_interface() else {
219            return Err(ScionSocketSendError::InvalidPacket(
220                "Can't determine egress interface for packet.".into(),
221            ));
222        };
223
224        // Lookup the address of the egress router by the source IA and egress interface.
225        let next_hop = match self
226            .underlay_discovery
227            .resolve_udp_underlay_next_hop(PathInterface {
228                isd_asn: source_ia,
229                id: egress_if,
230            }) {
231            Some(next_hop) => next_hop,
232            None => {
233                return Err(ScionSocketSendError::UnderlayNextHopUnreachable {
234                    isd_as: source_ia,
235                    interface_id: egress_if,
236                    address: None,
237                    msg: "next hop not found".to_string(),
238                });
239            }
240        };
241
242        self.socket
243            .try_send_to(packet.as_slice(), next_hop)
244            .map_err(|e| Self::map_send_io_error(e, source_ia, egress_if, next_hop))?;
245
246        Ok(())
247    }
248
249    async fn writeable(&self) {
250        // Ignore readiness errors; a subsequent `try_send` surfaces any real error.
251        let _ = self.socket.writable().await;
252    }
253
254    fn try_recv(&self, buf: &mut [u8]) -> Result<usize, ScionSocketReceiveError> {
255        loop {
256            let n = match self.socket.try_recv_from(buf) {
257                Ok((n, _)) => n,
258                Err(e) => return Err(ScionSocketReceiveError::IoError(e)),
259            };
260
261            // Only accept packets that decode and are addressed to this socket; skip the rest and
262            // try the next datagram.
263            match ScionRawPacketView::try_from_slice(&buf[..n]) {
264                Ok((packet, _rest)) => {
265                    match packet.dst_scion_addr() {
266                        Ok(dst) if dst == self.bind_addr.scion_addr() => return Ok(n),
267                        Ok(_) => {
268                            tracing::debug!(
269                                "Packet destination does not match assigned address, skipping"
270                            );
271                        }
272                        Err(e) => {
273                            tracing::error!(error = %e, "Failed to read destination address from packet");
274                        }
275                    }
276                }
277                Err(e) => {
278                    tracing::error!(error = %e, "Failed to decode SCION packet");
279                }
280            }
281        }
282    }
283
284    async fn readable(&self) {
285        // Ignore readiness errors; a subsequent `try_recv` surfaces any real error.
286        let _ = self.socket.readable().await;
287    }
288}