scion_sdk_address_manager/
manager.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//! IP address registry.
15
16use std::{
17    collections::BTreeMap,
18    net::IpAddr,
19    time::{self, Duration},
20};
21
22use ipnet::IpNet;
23use rand_chacha::ChaCha8Rng;
24use scion_proto::address::{EndhostAddr, IsdAsn};
25use serde::{Deserialize, Serialize};
26use thiserror::Error;
27
28use crate::allocator::{AddressAllocator, AddressAllocatorError, AllocatorCreationError};
29
30pub mod dto;
31
32const DEFAULT_HOLD_DURATION: Duration = Duration::from_secs(600);
33const MAX_ATTEMPTS: usize = 10;
34
35/// Address registration errors.
36#[derive(Debug, Error, PartialEq, Eq)]
37pub enum AddressRegistrationError {
38    /// Address already registered.
39    #[error("requested address {0} already registered")]
40    AddressAlreadyRegistered(EndhostAddr),
41    /// ISD-AS of the requested address not in range.
42    #[error("requested ISD-AS {0} not in allocation ISD-AS {1}")]
43    IaNotInAllocationRange(IsdAsn, IsdAsn),
44    /// Address allocation error.
45    #[error("requested address before start time")]
46    AddressAllocatorError(#[from] AddressAllocatorError),
47}
48
49/// AddressGrant is the result of a successful registration.
50#[derive(PartialEq, Eq, Debug, Clone)]
51pub struct AddressGrant {
52    /// Unique identifier of the allocation.
53    pub id: String,
54    /// ISD-AS.
55    pub isd_as: IsdAsn,
56    /// Allocated prefix.
57    pub prefix: ipnet::IpNet,
58}
59
60// Internal structure to keep track of grants
61#[derive(Debug, PartialEq, Serialize, Deserialize, Eq, Clone)]
62struct AddressGrantEntry {
63    /// The identity of the token that was used to retrieve this address grant.
64    id: String,
65    /// The endhost address that was allocated.
66    endhost_address: EndhostAddr,
67    // Time offset until the Grant will expire
68    on_hold_expiry: Option<Duration>,
69}
70
71/// Simple single ISD-AS address registry.
72#[derive(Debug, PartialEq, Eq, Clone)]
73pub struct AddressManager {
74    isd_as: IsdAsn,
75    hold_duration: time::Duration,
76    /// Map between ID and Grant
77    address_grants: BTreeMap<String, AddressGrantEntry>,
78    free_ips: AddressAllocator,
79    max_attempts: usize,
80    prefixes: Vec<IpNet>,
81}
82
83impl AddressManager {
84    /// Create a new address registry for the given ISD-AS and prefixes.
85    pub fn new(
86        isd_as: IsdAsn,
87        prefixes: Vec<IpNet>,
88        rng: ChaCha8Rng,
89    ) -> Result<Self, AllocatorCreationError> {
90        let free_ips = AddressAllocator::new(prefixes.clone(), rng)?;
91        Ok(Self {
92            isd_as,
93            hold_duration: DEFAULT_HOLD_DURATION,
94            address_grants: Default::default(),
95            max_attempts: MAX_ATTEMPTS,
96            free_ips,
97            prefixes,
98        })
99    }
100
101    /// Set the hold duration for address grants.
102    pub fn with_hold_duration(mut self, hold_duration: time::Duration) -> Self {
103        self.hold_duration = hold_duration;
104        self
105    }
106
107    /// Set the maximum number of attempts to allocate an address.
108    pub fn with_max_attempts(mut self, max_attempts: usize) -> Self {
109        self.max_attempts = max_attempts;
110        self
111    }
112
113    /// Checks if the given ISD-AS matches the registry's ISD-AS.
114    pub fn match_isd_as(&self, isd_as: IsdAsn) -> bool {
115        if isd_as.isd().is_wildcard() && isd_as.asn().is_wildcard() {
116            true
117        } else if isd_as.isd().is_wildcard() {
118            isd_as.asn() == self.isd_as.asn()
119        } else if isd_as.asn().is_wildcard() {
120            isd_as.isd() == self.isd_as.isd()
121        } else {
122            isd_as == self.isd_as
123        }
124    }
125
126    /// Registers a new Address, will remove current grant if `id` is reused
127    ///
128    /// ### Parameters
129    /// - `id`: Unique identifier of the allocation. One `id` can only have one Grant.
130    /// - `isd_asn`: The IsdAsn associated with this address allocation.
131    /// - `addr`: The requested IP address. Use [std::net::Ipv4Addr::UNSPECIFIED] or
132    ///   [std::net::Ipv6Addr::UNSPECIFIED] to request any available address.
133    pub fn register(
134        &mut self,
135        id: String,
136        mut isd_asn: IsdAsn,
137        addr: IpAddr,
138    ) -> Result<AddressGrant, AddressRegistrationError> {
139        if isd_asn.asn().is_wildcard() {
140            isd_asn.set_asn(self.isd_as.asn());
141        }
142        if isd_asn.isd().is_wildcard() {
143            isd_asn.set_isd(self.isd_as.isd());
144        }
145
146        if isd_asn != self.isd_as {
147            return Err(AddressRegistrationError::IaNotInAllocationRange(
148                isd_asn,
149                self.isd_as,
150            ));
151        }
152
153        let endhost_address = match (addr.is_unspecified(), self.address_grants.get(&id)) {
154            // Reuse Existing
155            (true, Some(existing))
156                // Only if they are of the same type
157                if existing.endhost_address.local_address().is_ipv4() == addr.is_ipv4() =>
158            {
159                existing.endhost_address
160            }
161            (false, Some(existing)) if existing.endhost_address.local_address() == addr => {
162                existing.endhost_address
163            }
164            // Try allocate new,
165            // if there was a different, existing alloc it will be removed further down
166            (..) => {
167                let ip = self.free_ips.allocate(addr)?;
168                EndhostAddr::new(isd_asn, ip)
169            }
170        };
171
172        let grant = AddressGrantEntry {
173            id: id.clone(),
174            on_hold_expiry: None,
175            endhost_address,
176        };
177
178        // Insert/Overwrite existing entry
179        if let Some(removed) = self.address_grants.insert(id, grant.clone()) {
180            if removed.endhost_address != endhost_address {
181                if let Err(e) = self.free_ips.free(removed.endhost_address.local_address()) {
182                    tracing::error!(
183                        "Allocator did not contain ip from removed entry - this should not happen: {e}"
184                    )
185                };
186            };
187        }
188
189        Ok(AddressGrant {
190            id: grant.id,
191            isd_as: grant.endhost_address.isd_asn(),
192            prefix: grant.endhost_address.local_address().into(),
193        })
194    }
195
196    /// Puts a grant on hold for the default hold period
197    ///
198    /// After this period has passed, the address can be freed by the cleanup job.
199    pub fn put_on_hold(&mut self, id: String, duration_since_start: Duration) -> bool {
200        match self.address_grants.get_mut(&id) {
201            Some(grant) => {
202                grant.on_hold_expiry = Some(duration_since_start + self.hold_duration);
203
204                true
205            }
206            None => false,
207        }
208    }
209
210    /// Immediately frees an address
211    ///
212    /// Returns `true` if address existed and was freed, otherwise `false`
213    pub fn free(&mut self, id: &String) -> bool {
214        match self.address_grants.remove(id) {
215            Some(removed) => {
216                if let Err(e) = self.free_ips.free(removed.endhost_address.local_address()) {
217                    tracing::warn!(
218                        "Address allocator did not contain an existing address grant - this should never happen: {e}"
219                    );
220                };
221
222                true
223            }
224            None => false,
225        }
226    }
227
228    /// Get this address registries isd-asn
229    pub fn isd_asn(&self) -> IsdAsn {
230        self.isd_as
231    }
232
233    /// Get this address registries prefixes
234    pub fn prefixes(&self) -> &[IpNet] {
235        &self.prefixes
236    }
237
238    /// Get the minimal count of free addresses of this registry
239    ///
240    /// Call [Self::clean_expired] before this to get the actual count
241    pub fn min_free_addresses(&self) -> u128 {
242        self.free_ips.free_v4() + self.free_ips.free_v6()
243    }
244
245    /// Checks all grants and removes expired grants
246    ///
247    /// Should be called periodically
248    ///
249    /// Returns (grant_count_before, grant_count_after)
250    pub fn clean_expired(&mut self, duration_since_start: Duration) -> (usize, usize) {
251        let start_grant_count = self.address_grants.len();
252
253        self.address_grants.retain(|_, grant| {
254            let Some(on_hold_until) = grant.on_hold_expiry else {
255                return true; // Skip not on hold grants
256            };
257
258            // If hold period has passed, clean
259            if duration_since_start > on_hold_until {
260                // Free IP alloc
261                self.free_ips
262                    .free(grant.endhost_address.local_address())
263                    .unwrap();
264
265                // Remove entry from grants
266                return false;
267            }
268
269            true
270        });
271
272        let end_grant_count = self.address_grants.len();
273        (start_grant_count, end_grant_count)
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use std::{
280        net::{Ipv4Addr, Ipv6Addr},
281        str::FromStr,
282    };
283
284    use rand::SeedableRng;
285    use rand_chacha::ChaCha8Rng;
286
287    use super::*;
288
289    fn duration(offset: u64) -> Duration {
290        Duration::from_secs(offset)
291    }
292
293    fn get_registry() -> AddressManager {
294        AddressManager::new(
295            IsdAsn::from_str("1-ff00:0:110").unwrap(),
296            vec![
297                IpNet::from_str("192.168.0.0/24").unwrap(),
298                IpNet::from_str("2001:db8::/64").unwrap(),
299            ],
300            ChaCha8Rng::seed_from_u64(42),
301        )
302        .unwrap()
303    }
304
305    fn id() -> String {
306        "00000000-0000-0000-0000-000000000001".to_string()
307    }
308
309    fn get_request(ip: &str) -> (IsdAsn, IpAddr) {
310        (
311            IsdAsn::from_str("1-ff00:0:110").unwrap(),
312            IpAddr::from_str(ip).unwrap(),
313        )
314    }
315
316    fn register(
317        id: String,
318        request: (IsdAsn, IpAddr),
319    ) -> Result<AddressGrant, AddressRegistrationError> {
320        get_registry().register(id, request.0, request.1)
321    }
322
323    #[test]
324    fn should_fail_on_isd_as_mismatch() {
325        let result = register(
326            id(),
327            (
328                IsdAsn::from_str("1-ff00:0:111").unwrap(),
329                "192.168.0.0".parse().unwrap(),
330            ),
331        );
332        assert_eq!(
333            result,
334            Err(AddressRegistrationError::IaNotInAllocationRange(
335                IsdAsn::from_str("1-ff00:0:111").unwrap(),
336                IsdAsn::from_str("1-ff00:0:110").unwrap()
337            ))
338        );
339    }
340
341    #[test]
342    fn should_fail_if_ipv4_is_outside_range() {
343        let result = register(id(), get_request("192.168.1.0"));
344        assert_eq!(
345            result,
346            Err(AddressRegistrationError::AddressAllocatorError(
347                AddressAllocatorError::AddressNotInPrefix("192.168.1.0".parse().unwrap())
348            ))
349        );
350    }
351
352    #[test]
353    fn should_fail_if_ipv6_is_outside_range() {
354        let result = register(id(), get_request("2001:db9::"));
355        assert_eq!(
356            result,
357            Err(AddressRegistrationError::AddressAllocatorError(
358                AddressAllocatorError::AddressNotInPrefix("2001:db9::".parse().unwrap())
359            ))
360        );
361    }
362
363    #[test]
364    fn should_succeed_to_re_register_same_ip_and_fail_register_same_ip_with_different_id() {
365        let isd_as = IsdAsn::from_str("1-ff00:0:110").unwrap();
366        let mut registry = get_registry();
367        // v4
368        {
369            let v4 = "192.168.0.0".parse().unwrap();
370            registry.register(id(), isd_as, v4).expect("Should Succeed");
371
372            // Register with the same id succeeds.
373            registry.register(id(), isd_as, v4).expect("Should Succeed");
374
375            // Register same address, with a different id fails.
376            let other_id = "Other Id".to_string();
377            let result = registry.register(other_id, isd_as, v4);
378            assert_eq!(
379                result,
380                Err(AddressRegistrationError::AddressAllocatorError(
381                    AddressAllocatorError::AddressAlreadyAllocated(v4)
382                )),
383                "got {result:?}"
384            );
385        }
386
387        // v6
388        {
389            let mut registry = get_registry();
390            let v6 = "2001:db8::".parse().unwrap();
391            registry.register(id(), isd_as, v6).expect("Should Succeed");
392
393            // Register with the same id succeeds.
394            registry.register(id(), isd_as, v6).expect("Should Succeed");
395
396            // Register same address, with a different id fails.
397            let other_id = "Other Id".to_string();
398            let result = registry.register(other_id, isd_as, v6);
399            assert_eq!(
400                result,
401                Err(AddressRegistrationError::AddressAllocatorError(
402                    AddressAllocatorError::AddressAlreadyAllocated(v6)
403                ))
404            );
405        }
406    }
407
408    #[test]
409    fn should_fail_if_no_address_is_available() {
410        let isd_as = IsdAsn::from_str("1-ff00:0:110").unwrap();
411        let mut registry = AddressManager::new(
412            IsdAsn::from_str("1-ff00:0:110").unwrap(),
413            vec![],
414            ChaCha8Rng::seed_from_u64(42),
415        )
416        .unwrap();
417        // v4
418        let v4 = Ipv4Addr::UNSPECIFIED.into();
419        let result = registry.register(id(), isd_as, v4);
420        assert_eq!(
421            result,
422            Err(AddressRegistrationError::AddressAllocatorError(
423                AddressAllocatorError::NoAddressesAvailable
424            ))
425        );
426
427        // v6
428        let v6 = Ipv6Addr::UNSPECIFIED.into();
429        let result = registry.register(id(), isd_as, v6);
430        assert_eq!(
431            result,
432            Err(AddressRegistrationError::AddressAllocatorError(
433                AddressAllocatorError::NoAddressesAvailable
434            ))
435        );
436    }
437
438    #[test]
439    fn should_succeed_allocation_with_specific_ip() {
440        let isd_as = IsdAsn::from_str("1-ff00:0:110").unwrap();
441        let mut registry = get_registry();
442        // v4
443        let v4 = "192.168.0.0".parse().unwrap();
444        let result = registry.register(id(), isd_as, v4).expect("Should succeed");
445        assert_eq!(result.prefix.addr(), v4, "Expected specific assignment");
446
447        // v6
448        let v6 = "2001:db8::".parse().unwrap();
449        let result = registry.register(id(), isd_as, v6).expect("Should succeed");
450        assert_eq!(result.prefix.addr(), v6, "Expected specific assignment");
451    }
452
453    #[test]
454    fn should_succeed_allocation_with_wildcard() {
455        let mut registry = get_registry();
456        // v4
457        let result = registry.register(id(), IsdAsn::WILDCARD, Ipv4Addr::UNSPECIFIED.into());
458        assert!(result.is_ok());
459
460        // v6
461        let result = registry.register(id(), IsdAsn::WILDCARD, Ipv6Addr::UNSPECIFIED.into());
462        assert!(result.is_ok());
463    }
464
465    #[test]
466    fn should_clean_existing_grant_on_reallocation() {
467        let mut registry = get_registry();
468        let initial = "192.168.0.0".parse().unwrap();
469        let other = "2001:db8::".parse().unwrap();
470
471        assert!(
472            registry.free_ips.is_free(initial),
473            "Expected initial address to be free"
474        );
475
476        // Try it a few times
477        for _ in 0..10 {
478            let grant = registry
479                .register(id(), IsdAsn::WILDCARD, initial)
480                .expect("Should succeed");
481
482            assert_eq!(
483                grant.prefix.addr(),
484                initial,
485                "Expected assignment of given address"
486            );
487
488            assert!(
489                !registry.free_ips.is_free(initial),
490                "Expected initial address to not be free"
491            );
492
493            assert!(
494                registry.free_ips.is_free(other),
495                "Expected other address to be free"
496            );
497            // Reregister with different ip
498            registry
499                .register(id(), IsdAsn::WILDCARD, other)
500                .expect("Should succeed");
501
502            assert!(
503                registry.free_ips.is_free(initial),
504                "Expected initial address to have been freed"
505            );
506        }
507    }
508
509    #[test]
510    fn should_not_assign_on_hold_address() {
511        let mut registry = get_registry();
512        let initial = "192.168.0.0".parse().unwrap();
513
514        let _grant = registry
515            .register(id(), IsdAsn::WILDCARD, initial)
516            .expect("Should succeed");
517
518        registry.put_on_hold(id(), duration(0));
519
520        let other_id = "Other Id".to_string();
521        let result = registry.register(other_id.clone(), IsdAsn::WILDCARD, initial);
522        assert_eq!(
523            result,
524            Err(AddressRegistrationError::AddressAllocatorError(
525                AddressAllocatorError::AddressAlreadyAllocated(initial)
526            )),
527            "Should have given AddressAlreadyAllocated got {result:?}"
528        );
529    }
530
531    #[test]
532    fn should_clean_expired_grants() {
533        let mut registry = get_registry();
534        let initial = "192.168.0.0".parse().unwrap();
535        let grant = registry
536            .register(id(), IsdAsn::WILDCARD, initial)
537            .expect("Should succeed");
538
539        assert!(registry.put_on_hold(grant.id, duration(0)));
540
541        let (before, after) =
542            registry.clean_expired(registry.hold_duration + Duration::from_nanos(1));
543        assert_eq!(before, 1);
544        assert_eq!(after, 0, "Expected grant to have been cleaned");
545        assert!(
546            registry.free_ips.is_free(initial),
547            "Expected initial address to have been freed"
548        );
549    }
550
551    #[test]
552    fn should_keep_not_expired_grants() {
553        let mut registry = get_registry();
554
555        let initial = "192.168.0.0".parse().unwrap();
556        let grant = registry
557            .register(id(), IsdAsn::WILDCARD, initial)
558            .expect("Should succeed");
559
560        assert!(registry.put_on_hold(grant.id, duration(0)));
561
562        let (before, after) = registry.clean_expired(registry.hold_duration);
563        assert_eq!(before, 1);
564        assert_eq!(after, 1);
565    }
566}