Skip to main content

snap_tun/
requests.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//! SNAP tunnel control requests.
15
16use std::{
17    net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
18    time::SystemTime,
19};
20
21use ipnet::{IpNet, Ipv4Net, Ipv6Net};
22use scion_proto::address::{EndhostAddr, IsdAsn};
23
24pub(crate) fn system_time_from_unix_epoch_secs(secs: u64) -> std::time::SystemTime {
25    std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs)
26}
27
28pub(crate) fn unix_epoch_from_system_time(time: SystemTime) -> u64 {
29    time.duration_since(SystemTime::UNIX_EPOCH)
30        .unwrap_or_default()
31        .as_secs()
32}
33
34/// Response to a token update request.
35#[derive(Clone, PartialEq, ::prost::Message)]
36pub struct TokenUpdateResponse {
37    /// The unix epoch timestamp at which the token expires.
38    #[prost(uint64, tag = "1")]
39    pub valid_until: u64,
40}
41
42/// Represents a SCION endhost address range.
43#[derive(Clone, PartialEq, ::prost::Message)]
44pub struct AddressRange {
45    /// The ISD-AS of the requested address. Can include wildcards.
46    #[prost(uint64, tag = "1")]
47    pub isd_as: u64,
48    /// Version MUST be either 4 or 6, indicating IPv4 or IPv6 respectively.
49    #[prost(uint32, tag = "2")]
50    pub ip_version: u32,
51    /// The length of the network prefix. May not be larger than 32
52    /// for version = 4, and may not be larger than 128 for version =
53    /// 6.
54    #[prost(uint32, tag = "3")]
55    pub prefix_length: u32,
56    /// The IP address in network byte order. The length of the address
57    /// must be 4 for version = 4 and 16 for version = 16.
58    #[prost(bytes = "vec", tag = "4")]
59    pub address: Vec<u8>,
60}
61
62impl AddressRange {
63    pub(crate) fn ipnet(&self) -> Result<IpNet, AddrError> {
64        match self.ip_version {
65            4 => {
66                if self.prefix_length != 32 {
67                    return Err(AddrError::InvalidPrefixLen {
68                        actual: self.prefix_length as u8,
69                        max: 32,
70                    });
71                }
72                if self.address.len() != 4 {
73                    return Err(AddrError::InvalidAddressLen {
74                        actual: self.address.len() as u8,
75                        expected: 4,
76                    });
77                }
78                let mut bytes = [0u8; 4];
79                bytes[..].copy_from_slice(&self.address[..]);
80                Ok(Ipv4Net::new_assert(Ipv4Addr::from(bytes), self.prefix_length as u8).into())
81            }
82            6 => {
83                if self.prefix_length != 128 {
84                    return Err(AddrError::InvalidPrefixLen {
85                        actual: self.prefix_length as u8,
86                        max: 128,
87                    });
88                }
89                if self.address.len() != 16 {
90                    return Err(AddrError::InvalidAddressLen {
91                        actual: self.address.len() as u8,
92                        expected: 16,
93                    });
94                }
95                let mut bytes = [0u8; 16];
96                bytes[..].copy_from_slice(&self.address[..]);
97                Ok(Ipv6Net::new_assert(Ipv6Addr::from(bytes), self.prefix_length as u8).into())
98            }
99            v => Err(AddrError::InvalidIPVersion(v)),
100        }
101    }
102}
103
104/// Represents a socket addr assignment request.
105#[derive(Clone, PartialEq, ::prost::Message)]
106pub struct SocketAddrAssignmentRequest {}
107
108/// Represents a socket addr assignment response.
109#[derive(Clone, PartialEq, ::prost::Message)]
110pub struct SocketAddrAssignmentResponse {
111    /// Version MUST be either 4 or 6, indicating IPv4 or IPv6 respectively.
112    #[prost(uint32, tag = "1")]
113    pub ip_version: u32,
114    /// The IP address in network byte order. The length of the address
115    /// must be 4 for version = 4 and 16 for version = 6.
116    #[prost(bytes = "vec", tag = "2")]
117    pub address: Vec<u8>,
118    /// The port of the endhost socket address.
119    #[prost(uint32, tag = "3")]
120    pub port: u32,
121}
122
123impl SocketAddrAssignmentResponse {
124    /// Converts the response to a standard `std::net::SocketAddr`.
125    pub fn socket_addr(&self) -> Result<SocketAddr, AddrError> {
126        let port = self
127            .port
128            .try_into()
129            .map_err(|_| AddrError::InvalidPort(self.port))?;
130
131        match self.ip_version {
132            4 => {
133                if self.address.len() != 4 {
134                    return Err(AddrError::InvalidAddressLen {
135                        actual: self.address.len() as u8,
136                        expected: 4,
137                    });
138                }
139                let mut bytes = [0u8; 4];
140                bytes.copy_from_slice(&self.address);
141                let addr = Ipv4Addr::from(bytes);
142                Ok(SocketAddr::new(addr.into(), port))
143            }
144            6 => {
145                if self.address.len() != 16 {
146                    return Err(AddrError::InvalidAddressLen {
147                        actual: self.address.len() as u8,
148                        expected: 16,
149                    });
150                }
151                let mut bytes = [0u8; 16];
152                bytes.copy_from_slice(&self.address);
153                let addr = Ipv6Addr::from(bytes);
154                Ok(SocketAddr::new(addr.into(), port))
155            }
156            v => Err(AddrError::InvalidIPVersion(v)),
157        }
158    }
159}
160
161impl From<SocketAddr> for SocketAddrAssignmentResponse {
162    fn from(socket_addr: SocketAddr) -> Self {
163        match socket_addr {
164            SocketAddr::V4(addr) => {
165                Self {
166                    ip_version: 4,
167                    address: addr.ip().octets().to_vec(),
168                    port: addr.port() as u32,
169                }
170            }
171            SocketAddr::V6(addr) => {
172                Self {
173                    ip_version: 6,
174                    address: addr.ip().octets().to_vec(),
175                    port: addr.port() as u32,
176                }
177            }
178        }
179    }
180}
181
182impl TryInto<EndhostAddr> for &AddressRange {
183    type Error = AddrError;
184
185    fn try_into(self) -> Result<EndhostAddr, Self::Error> {
186        let addr: IpNet = self.ipnet()?;
187        let isd_as = IsdAsn::from(self.isd_as);
188        if isd_as.is_wildcard() {
189            return Err(AddrError::InvalidIsdAs);
190        }
191        Ok(EndhostAddr::new(isd_as, addr.addr()))
192    }
193}
194
195impl TryInto<(IsdAsn, IpNet)> for &AddressRange {
196    type Error = AddrError;
197
198    fn try_into(self) -> Result<(IsdAsn, IpNet), Self::Error> {
199        let addr: IpNet = self.ipnet()?;
200        let isd_as = IsdAsn::from(self.isd_as);
201        if isd_as.is_wildcard() {
202            return Err(AddrError::InvalidIsdAs);
203        }
204        Ok((isd_as, addr))
205    }
206}
207
208impl From<&EndhostAddr> for AddressRange {
209    fn from(addr: &EndhostAddr) -> Self {
210        let isd_as = addr.isd_asn().to_u64();
211        let (ip_version, prefix_length, address) = match addr.local_address() {
212            IpAddr::V4(a) => (4, 32, a.octets().to_vec()),
213            IpAddr::V6(a) => (6, 128, a.octets().to_vec()),
214        };
215        AddressRange {
216            isd_as,
217            ip_version,
218            prefix_length,
219            address,
220        }
221    }
222}
223
224/// SNAP tunnel address errors.
225#[derive(Debug, thiserror::Error)]
226pub enum AddrError {
227    /// Unsupported IP version.
228    #[error("unsupported IP version {0}")]
229    InvalidIPVersion(u32),
230    /// Invalid address length.
231    #[error("invalid address length")]
232    InvalidAddressLen {
233        /// Provided length.
234        actual: u8,
235        /// Expected length.
236        expected: u8,
237    },
238    /// Invalid prefix length.
239    #[error("invalid prefix length")]
240    InvalidPrefixLen {
241        /// Provided length.
242        actual: u8,
243        /// Maximum allowed length.
244        max: u8,
245    },
246    /// Wildcard ISD-AS is not allowed.
247    #[error("wildcard ISD-AS is not allowed")]
248    InvalidIsdAs,
249    /// Provided port is outside of allowed range 1..65535.
250    #[error("port outside of allowed range: {0}")]
251    InvalidPort(u32),
252}
253
254#[cfg(test)]
255mod tests {
256    use std::net::IpAddr;
257
258    use assert_matches::assert_matches;
259    use scion_proto::address::{Asn, Isd};
260
261    use super::*;
262
263    const TEST_ISD_AS: IsdAsn = IsdAsn::new(Isd(1), Asn::new(0xff00_0000_0110));
264
265    #[test]
266    fn try_into_endhost_addr_ipv4_success() {
267        let address_range = AddressRange {
268            isd_as: TEST_ISD_AS.to_u64(),
269            ip_version: 4,
270            prefix_length: 32,
271            address: vec![192, 0, 2, 1],
272        };
273
274        let result: Result<EndhostAddr, _> = (&address_range).try_into();
275        let endhost_addr = result.expect("conversion should succeed");
276
277        let expected_addr = EndhostAddr::new(TEST_ISD_AS, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)));
278        assert_eq!(endhost_addr, expected_addr);
279    }
280
281    #[test]
282    fn try_into_endhost_addr_ipv6_success() {
283        let address_range = AddressRange {
284            isd_as: TEST_ISD_AS.to_u64(),
285            ip_version: 6,
286            prefix_length: 128,
287            address: vec![
288                0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70,
289                0x73, 0x34,
290            ],
291        };
292
293        let result: Result<EndhostAddr, _> = (&address_range).try_into();
294        let endhost_addr = result.expect("conversion should succeed");
295
296        let expected_addr = EndhostAddr::new(
297            TEST_ISD_AS,
298            IpAddr::V6(Ipv6Addr::new(
299                0x2001, 0x0db8, 0x85a3, 0, 0, 0x8a2e, 0x0370, 0x7334,
300            )),
301        );
302        assert_eq!(endhost_addr, expected_addr);
303    }
304
305    #[test]
306    fn try_into_endhost_addr_invalid_ip_version() {
307        let address_range = AddressRange {
308            isd_as: TEST_ISD_AS.to_u64(),
309            ip_version: 5, // Invalid version
310            prefix_length: 32,
311            address: vec![192, 0, 2, 1],
312        };
313
314        let result: Result<EndhostAddr, _> = (&address_range).try_into();
315        assert_matches!(result, Err(AddrError::InvalidIPVersion(5)));
316    }
317
318    #[test]
319    fn try_into_endhost_addr_ipv4_invalid_prefix() {
320        let address_range = AddressRange {
321            isd_as: TEST_ISD_AS.to_u64(),
322            ip_version: 4,
323            prefix_length: 24, // Invalid prefix for endhost
324            address: vec![192, 0, 2, 1],
325        };
326
327        let result: Result<EndhostAddr, _> = (&address_range).try_into();
328        assert_matches!(
329            result,
330            Err(AddrError::InvalidPrefixLen {
331                actual: 24,
332                max: 32
333            })
334        );
335    }
336
337    #[test]
338    fn try_into_endhost_addr_ipv6_invalid_prefix() {
339        let address_range = AddressRange {
340            isd_as: TEST_ISD_AS.to_u64(),
341            ip_version: 6,
342            prefix_length: 64, // Invalid prefix for endhost
343            address: vec![0; 16],
344        };
345
346        let result: Result<EndhostAddr, _> = (&address_range).try_into();
347        assert_matches!(
348            result,
349            Err(AddrError::InvalidPrefixLen {
350                actual: 64,
351                max: 128
352            })
353        );
354    }
355
356    #[test]
357    fn try_into_endhost_addr_ipv4_invalid_addr_len() {
358        let address_range = AddressRange {
359            isd_as: TEST_ISD_AS.to_u64(),
360            ip_version: 4,
361            prefix_length: 32,
362            address: vec![192, 0, 2], // Invalid length
363        };
364
365        let result: Result<EndhostAddr, _> = (&address_range).try_into();
366        assert_matches!(
367            result,
368            Err(AddrError::InvalidAddressLen {
369                actual: 3,
370                expected: 4
371            })
372        );
373    }
374
375    #[test]
376    fn try_into_endhost_addr_ipv6_invalid_addr_len() {
377        let address_range = AddressRange {
378            isd_as: TEST_ISD_AS.to_u64(),
379            ip_version: 6,
380            prefix_length: 128,
381            address: vec![0; 15], // Invalid length
382        };
383
384        let result: Result<EndhostAddr, _> = (&address_range).try_into();
385        assert_matches!(
386            result,
387            Err(AddrError::InvalidAddressLen {
388                actual: 15,
389                expected: 16
390            })
391        );
392    }
393
394    #[test]
395    fn try_into_endhost_addr_wildcard_isd_as() {
396        let address_range = AddressRange {
397            isd_as: IsdAsn::WILDCARD.to_u64(), // Wildcard ISD-AS
398            ip_version: 4,
399            prefix_length: 32,
400            address: vec![192, 0, 2, 1],
401        };
402
403        let result: Result<EndhostAddr, _> = (&address_range).try_into();
404        assert_matches!(result, Err(AddrError::InvalidIsdAs));
405    }
406}