Skip to main content

snap_control/server/
identity_registry.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//! SNAPtun static identity registry.
15
16use std::{
17    collections::{BTreeMap, HashSet},
18    sync::Arc,
19    time::{Duration, Instant},
20};
21
22use snap_tun::server::SnapTunAuthorization;
23
24use crate::crpc_api::api_service::model::SnapTunIdentityRegistry;
25
26type Identity = [u8; 32];
27
28#[derive(Default, Clone)]
29struct IdentityRegistryState {
30    /// Map socket addresses to their associated identity and the time it was last active.
31    // Note: We point to an IdentityRegistration in order to avoid copying the
32    // values of the entries.
33    pub associations: BTreeMap<Arc<str>, Arc<Identity>>,
34    pub expiry: BTreeMap<Arc<Identity>, Instant>,
35}
36
37impl IdentityRegistryState {
38    pub(crate) fn is_authorized(&self, now: Instant, ident: &Identity) -> bool {
39        self.expiry
40            .get(ident)
41            .map(|expiry| *expiry > now)
42            .unwrap_or(false)
43    }
44
45    /// Returns true if the identity existed before.
46    pub(crate) fn add_identity<S: AsRef<str>>(
47        &mut self,
48        key: S,
49        identity: Identity,
50        expiry: Instant,
51    ) -> bool {
52        let key = Arc::<str>::from(key.as_ref().to_string());
53        let ident = Arc::new(identity);
54        if let Some(prev_ident) = self.associations.insert(key.clone(), ident.clone())
55            && prev_ident != ident
56        {
57            self.expiry.remove(&prev_ident);
58        }
59        self.expiry.insert(ident, expiry).is_none()
60    }
61
62    /// Removes all expired entries.
63    pub(crate) fn clean_expired(&mut self, now: Instant) {
64        let mut removed: HashSet<Arc<Identity>> = Default::default();
65        self.expiry.retain(|ident, expiry| {
66            if *expiry <= now {
67                removed.insert(ident.clone());
68                return false;
69            }
70            true
71        });
72        self.associations
73            .retain(|_, ident| !removed.contains(ident));
74    }
75}
76
77/// Registrar for SNAPtun static identities.
78pub struct IdentityRegistry {
79    // By using an ArcSwap we optimize for read latency at the (relatively
80    // heavy) price of copying the entire map when doing an update. We assume
81    // that this is ok for now, but recommend keeping track of latencies in
82    // production.
83    //
84    // Alternatively, the size of this map should be kept small.
85    state: arc_swap::ArcSwap<IdentityRegistryState>,
86}
87
88impl IdentityRegistry {
89    /// Creates a new identity registry with the given keepalive interval.
90    #[allow(clippy::new_without_default)]
91    pub fn new() -> Self {
92        Self {
93            state: Default::default(),
94        }
95    }
96
97    /// Returns `true` iff the `identity` is authorized to send packets at time
98    /// `now`.
99    ///
100    /// Eventually, this method should return the PSK under which the identity
101    /// is authorized.
102    pub fn is_authorized(&self, now: Instant, identity: &Identity) -> bool {
103        self.state.load().is_authorized(now, identity)
104    }
105
106    /// Registers a new identity, associated with key `key` and with the given
107    /// lifetime. There can be at most one identity registered per key. If an
108    /// identity already exists, it is overwritten. The method is indempotent.
109    ///
110    /// # Return value
111    ///
112    /// Returns true if no registration existed before; otherwise false.
113    pub fn register<S: AsRef<str>>(
114        &self,
115        now: Instant,
116        key: S,
117        ident: Identity,
118        lifetime: Duration,
119    ) -> bool {
120        let mut res = false;
121        self.update_state(|state| {
122            res = state.add_identity(key, ident, now + lifetime);
123        });
124        res
125    }
126
127    /// Removes all expired entries.
128    pub fn remove_expired(&self, now: Instant) {
129        self.update_state(|state| state.clean_expired(now));
130    }
131
132    fn update_state<F>(&self, modifier: F)
133    where
134        F: FnOnce(&mut IdentityRegistryState),
135    {
136        // As cache locality is lost when copying complex data structures, the
137        // win in terms of being lock-less might actually be eaten up again.
138        let mut state: IdentityRegistryState = (**self.state.load()).clone();
139        (modifier)(&mut state);
140        self.state.store(Arc::new(state))
141    }
142
143    #[cfg(test)]
144    pub(crate) fn ident_exist(&self, ident: &Identity) -> bool {
145        self.state
146            .load()
147            .associations
148            .values()
149            .any(|v| v.as_ref() == ident)
150            || self.state.load().expiry.keys().any(|k| k.as_ref() == ident)
151    }
152}
153
154impl SnapTunIdentityRegistry for IdentityRegistry {
155    fn register(
156        &self,
157        now: Instant,
158        key: &str,
159        identity: Identity,
160        _psk_share: Option<[u8; 32]>,
161        lifetime: Duration,
162    ) -> bool {
163        self.register(now, key, identity, lifetime)
164    }
165}
166
167impl SnapTunAuthorization for IdentityRegistry {
168    fn is_authorized(&self, now: Instant, identity: &Identity) -> bool {
169        self.is_authorized(now, identity)
170    }
171}
172
173#[cfg(test)]
174mod tests {
175    use x25519_dalek::PublicKey;
176
177    use super::*;
178
179    fn create_test_identity(seed: u8) -> PublicKey {
180        let mut bytes = [0u8; 32];
181        bytes[0] = seed;
182        PublicKey::from(bytes)
183    }
184
185    #[test]
186    fn test_identity_not_registered() {
187        let registry = IdentityRegistry::new();
188        let now = Instant::now();
189        let identity = create_test_identity(1);
190
191        assert!(!registry.is_authorized(now, identity.as_bytes()));
192    }
193
194    #[test]
195    fn test_identity_is_authorized_before_expires() {
196        let registry = IdentityRegistry::new();
197        let now = Instant::now();
198        let identity = create_test_identity(1);
199
200        registry.register(now, "", *identity.as_bytes(), Duration::from_secs(30));
201
202        assert!(registry.is_authorized(now, identity.as_bytes()));
203    }
204
205    #[test]
206    fn test_reregistering_identity_returns_false_but_succeeds() {
207        let registry = IdentityRegistry::new();
208        let now = Instant::now();
209        let identity = create_test_identity(1);
210        let delta_t = Duration::from_secs(10);
211
212        registry.register(now, "", *identity.as_bytes(), delta_t);
213        assert!(!registry.is_authorized(now + delta_t, identity.as_bytes()));
214        assert!(!registry.register(now, "", *identity.as_bytes(), 2 * delta_t));
215        assert!(registry.is_authorized(now + delta_t, identity.as_bytes()));
216    }
217
218    #[test]
219    fn test_identity_is_unauthorized_at_expiry() {
220        let registry = IdentityRegistry::new();
221        let now = Instant::now();
222        let identity = create_test_identity(1);
223        let delta_t = Duration::from_secs(30);
224
225        registry.register(now, "", *identity.as_bytes(), delta_t);
226
227        assert!(!registry.is_authorized(now + delta_t, identity.as_bytes()));
228    }
229
230    #[test]
231    fn test_identity_is_removed_after_expiry() {
232        let registry = IdentityRegistry::new();
233        let now = Instant::now();
234        let identity = create_test_identity(1);
235        let delta_t = Duration::from_secs(30);
236
237        registry.register(now, "", *identity.as_bytes(), delta_t);
238        assert!(registry.ident_exist(identity.as_bytes()));
239        registry.remove_expired(now + delta_t);
240        assert!(!registry.ident_exist(identity.as_bytes()));
241    }
242}