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},
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 session renewal request.
35#[derive(Clone, PartialEq, ::prost::Message)]
36pub struct SessionRenewalResponse {
37    /// The unix epoch timestamp at which this session 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 an address assignment request.
105#[derive(Clone, PartialEq, ::prost::Message)]
106pub struct AddressAssignRequest {
107    /// The requested endhost address ranges.
108    #[prost(message, repeated, tag = "1")]
109    pub requested_addresses: Vec<AddressRange>,
110}
111
112/// Response to a address assign request.
113#[derive(Clone, PartialEq, ::prost::Message)]
114pub struct AddressAssignResponse {
115    /// The assigned address ranges.
116    #[prost(message, repeated, tag = "1")]
117    pub assigned_addresses: Vec<AddressRange>,
118}
119
120impl TryInto<EndhostAddr> for &AddressRange {
121    type Error = AddrError;
122
123    fn try_into(self) -> Result<EndhostAddr, Self::Error> {
124        let addr: IpNet = self.ipnet()?;
125        let isd_as = IsdAsn::from(self.isd_as);
126        if isd_as.is_wildcard() {
127            return Err(AddrError::InvalidIsdAs);
128        }
129        Ok(EndhostAddr::new(isd_as, addr.addr()))
130    }
131}
132
133impl TryInto<(IsdAsn, IpNet)> for &AddressRange {
134    type Error = AddrError;
135
136    fn try_into(self) -> Result<(IsdAsn, IpNet), Self::Error> {
137        let addr: IpNet = self.ipnet()?;
138        let isd_as = IsdAsn::from(self.isd_as);
139        if isd_as.is_wildcard() {
140            return Err(AddrError::InvalidIsdAs);
141        }
142        Ok((isd_as, addr))
143    }
144}
145
146impl From<&EndhostAddr> for AddressRange {
147    fn from(addr: &EndhostAddr) -> Self {
148        let isd_as = addr.isd_asn().to_u64();
149        let (ip_version, prefix_length, address) = match addr.local_address() {
150            IpAddr::V4(a) => (4, 32, a.octets().to_vec()),
151            IpAddr::V6(a) => (6, 128, a.octets().to_vec()),
152        };
153        AddressRange {
154            isd_as,
155            ip_version,
156            prefix_length,
157            address,
158        }
159    }
160}
161
162/// SNAP tunnel address errors.
163#[derive(Debug, thiserror::Error)]
164pub enum AddrError {
165    /// Unsupported IP version.
166    #[error("unsupported IP version {0}")]
167    InvalidIPVersion(u32),
168    /// Invalid address length.
169    #[error("invalid address length")]
170    InvalidAddressLen {
171        /// Provided length.
172        actual: u8,
173        /// Expected length.
174        expected: u8,
175    },
176    /// Invalid prefix length.
177    #[error("invalid prefix length")]
178    InvalidPrefixLen {
179        /// Provided length.
180        actual: u8,
181        /// Maximum allowed length.
182        max: u8,
183    },
184    /// Wildcard ISD-AS is not allowed.
185    #[error("wildcard ISD-AS is not allowed")]
186    InvalidIsdAs,
187}
188
189#[cfg(test)]
190mod tests {
191    use std::net::IpAddr;
192
193    use assert_matches::assert_matches;
194    use scion_proto::address::{Asn, Isd};
195
196    use super::*;
197
198    const TEST_ISD_AS: IsdAsn = IsdAsn::new(Isd(1), Asn::new(0xff00_0000_0110));
199
200    #[test]
201    fn try_into_endhost_addr_ipv4_success() {
202        let address_range = AddressRange {
203            isd_as: TEST_ISD_AS.to_u64(),
204            ip_version: 4,
205            prefix_length: 32,
206            address: vec![192, 0, 2, 1],
207        };
208
209        let result: Result<EndhostAddr, _> = (&address_range).try_into();
210        let endhost_addr = result.expect("conversion should succeed");
211
212        let expected_addr = EndhostAddr::new(TEST_ISD_AS, IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)));
213        assert_eq!(endhost_addr, expected_addr);
214    }
215
216    #[test]
217    fn try_into_endhost_addr_ipv6_success() {
218        let address_range = AddressRange {
219            isd_as: TEST_ISD_AS.to_u64(),
220            ip_version: 6,
221            prefix_length: 128,
222            address: vec![
223                0x20, 0x01, 0x0d, 0xb8, 0x85, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x2e, 0x03, 0x70,
224                0x73, 0x34,
225            ],
226        };
227
228        let result: Result<EndhostAddr, _> = (&address_range).try_into();
229        let endhost_addr = result.expect("conversion should succeed");
230
231        let expected_addr = EndhostAddr::new(
232            TEST_ISD_AS,
233            IpAddr::V6(Ipv6Addr::new(
234                0x2001, 0x0db8, 0x85a3, 0, 0, 0x8a2e, 0x0370, 0x7334,
235            )),
236        );
237        assert_eq!(endhost_addr, expected_addr);
238    }
239
240    #[test]
241    fn try_into_endhost_addr_invalid_ip_version() {
242        let address_range = AddressRange {
243            isd_as: TEST_ISD_AS.to_u64(),
244            ip_version: 5, // Invalid version
245            prefix_length: 32,
246            address: vec![192, 0, 2, 1],
247        };
248
249        let result: Result<EndhostAddr, _> = (&address_range).try_into();
250        assert_matches!(result, Err(AddrError::InvalidIPVersion(5)));
251    }
252
253    #[test]
254    fn try_into_endhost_addr_ipv4_invalid_prefix() {
255        let address_range = AddressRange {
256            isd_as: TEST_ISD_AS.to_u64(),
257            ip_version: 4,
258            prefix_length: 24, // Invalid prefix for endhost
259            address: vec![192, 0, 2, 1],
260        };
261
262        let result: Result<EndhostAddr, _> = (&address_range).try_into();
263        assert_matches!(
264            result,
265            Err(AddrError::InvalidPrefixLen {
266                actual: 24,
267                max: 32
268            })
269        );
270    }
271
272    #[test]
273    fn try_into_endhost_addr_ipv6_invalid_prefix() {
274        let address_range = AddressRange {
275            isd_as: TEST_ISD_AS.to_u64(),
276            ip_version: 6,
277            prefix_length: 64, // Invalid prefix for endhost
278            address: vec![0; 16],
279        };
280
281        let result: Result<EndhostAddr, _> = (&address_range).try_into();
282        assert_matches!(
283            result,
284            Err(AddrError::InvalidPrefixLen {
285                actual: 64,
286                max: 128
287            })
288        );
289    }
290
291    #[test]
292    fn try_into_endhost_addr_ipv4_invalid_addr_len() {
293        let address_range = AddressRange {
294            isd_as: TEST_ISD_AS.to_u64(),
295            ip_version: 4,
296            prefix_length: 32,
297            address: vec![192, 0, 2], // Invalid length
298        };
299
300        let result: Result<EndhostAddr, _> = (&address_range).try_into();
301        assert_matches!(
302            result,
303            Err(AddrError::InvalidAddressLen {
304                actual: 3,
305                expected: 4
306            })
307        );
308    }
309
310    #[test]
311    fn try_into_endhost_addr_ipv6_invalid_addr_len() {
312        let address_range = AddressRange {
313            isd_as: TEST_ISD_AS.to_u64(),
314            ip_version: 6,
315            prefix_length: 128,
316            address: vec![0; 15], // Invalid length
317        };
318
319        let result: Result<EndhostAddr, _> = (&address_range).try_into();
320        assert_matches!(
321            result,
322            Err(AddrError::InvalidAddressLen {
323                actual: 15,
324                expected: 16
325            })
326        );
327    }
328
329    #[test]
330    fn try_into_endhost_addr_wildcard_isd_as() {
331        let address_range = AddressRange {
332            isd_as: IsdAsn::WILDCARD.to_u64(), // Wildcard ISD-AS
333            ip_version: 4,
334            prefix_length: 32,
335            address: vec![192, 0, 2, 1],
336        };
337
338        let result: Result<EndhostAddr, _> = (&address_range).try_into();
339        assert_matches!(result, Err(AddrError::InvalidIsdAs));
340    }
341}