1use crate::configuration::ServerParameters;
6use crate::protocol::identifier::ClientIdentifier;
7use crate::protocol::{DhcpOption, OptionCode};
8use crate::server::{ClientRecords, DataStore, LeaseRecord};
9use log::warn;
10use std::collections::{HashMap, HashSet};
11use std::str::FromStr as _;
12use std::string::ToString as _;
13
14#[derive(Clone, Debug)]
23pub struct Stash {
24 prefix: String,
25 proxy: fidl_fuchsia_stash::StoreAccessorProxy,
26}
27
28#[derive(Debug, thiserror::Error)]
29pub enum StashError {
30 #[error("stash initialized with empty prefix")]
31 EmptyPrefix,
32 #[error(
33 "stash initialized with prefix={prefix} containing invalid character(s) {invalid_chars:?}"
34 )]
35 InvalidPrefix { prefix: String, invalid_chars: HashSet<char> },
36 #[error("unexpected value variant stored in stash: {actual:?}, expected {expected:?}")]
37 UnexpectedStashValue { actual: fidl_fuchsia_stash::Value, expected: fidl_fuchsia_stash::Value },
38 #[error("failed to deserialize json string={0}")]
39 JsonDeserialization(String, #[source] serde_json::Error),
40 #[error("failed to serialize to json for key={0}")]
41 JsonSerialization(String, #[source] serde_json::Error),
42 #[error("stash does not contain value for key={0}")]
43 MissingValue(String),
44 #[error("FIDL call to stash failed: {0}")]
45 Fidl(#[from] fidl::Error),
46 #[error("connecting to stash failed")]
47 StashConnect(#[source] anyhow::Error),
48}
49
50impl DataStore for Stash {
51 type Error = StashError;
52
53 fn insert(
54 &mut self,
55 client_id: &ClientIdentifier,
56 client_record: &LeaseRecord,
57 ) -> Result<(), Self::Error> {
58 self.store(&self.client_key(client_id), client_record)
59 }
60
61 fn store_options(&mut self, opts: &[DhcpOption]) -> Result<(), Self::Error> {
62 self.store(OPTIONS_KEY, opts)
63 }
64
65 fn store_parameters(&mut self, params: &ServerParameters) -> Result<(), Self::Error> {
66 self.store(PARAMETERS_KEY, params)
67 }
68
69 fn delete(&mut self, client_id: &ClientIdentifier) -> Result<(), Self::Error> {
70 self.rm_key(&self.client_key(client_id))
71 }
72}
73
74const OPTIONS_KEY: &'static str = "options";
75const PARAMETERS_KEY: &'static str = "parameters";
76const CLIENT_KEY_PREFIX: &'static str = "client";
77
78impl Stash {
79 pub fn new(id: &str) -> Result<Self, StashError> {
84 Self::new_with_prefix(id, CLIENT_KEY_PREFIX)
85 }
86
87 fn new_with_prefix(id: &str, prefix: &str) -> Result<Self, StashError> {
88 if prefix.is_empty() {
89 return Err(StashError::EmptyPrefix);
90 }
91 let invalid_chars: HashSet<char> =
92 prefix.matches(&['-', ':'][..]).map(|s| char::from_str(s).unwrap()).collect();
93 if !invalid_chars.is_empty() {
94 return Err(StashError::InvalidPrefix { prefix: prefix.to_string(), invalid_chars });
95 }
96 let store_client = fuchsia_component::client::connect_to_protocol::<
97 fidl_fuchsia_stash::SecureStoreMarker,
98 >()
99 .map_err(StashError::StashConnect)?;
100 let () = store_client.identify(id)?;
101 let (proxy, accessor_server) =
102 fidl::endpoints::create_proxy::<fidl_fuchsia_stash::StoreAccessorMarker>();
103 let () = store_client.create_accessor(false, accessor_server)?;
104 let prefix = prefix.to_string();
105 Ok(Stash { prefix, proxy })
106 }
107
108 fn store<T>(&self, key: &str, v: &T) -> Result<(), StashError>
109 where
110 T: serde::Serialize + std::fmt::Debug + ?Sized,
111 {
112 let v = fidl_fuchsia_stash::Value::Stringval(
113 serde_json::to_string(v)
114 .map_err(|e| StashError::JsonSerialization(key.to_string(), e))?,
115 );
116 let () = self.proxy.set_value(key, v)?;
117 let () = self.proxy.commit()?;
118 Ok(())
119 }
120
121 pub async fn load_client_records(&self) -> Result<ClientRecords, StashError> {
128 use futures::TryStreamExt as _;
129
130 let (iter, server) =
131 fidl::endpoints::create_proxy::<fidl_fuchsia_stash::GetIteratorMarker>();
132 let () = self.proxy.get_prefix(&self.prefix, server)?;
133 futures::stream::try_unfold(iter, |iter| async move {
134 let kvs = iter.get_next().await?;
135 let yielded = (!kvs.is_empty()).then_some((kvs, iter));
136 Result::<_, StashError>::Ok(yielded)
137 })
138 .map_ok(|kvs| futures::stream::iter(kvs.into_iter().map(Ok)))
139 .try_flatten()
140 .try_filter_map(|kv| async move {
141 let key = match kv.key.split("-").last() {
142 Some(v) => v,
143 None => {
144 warn!("failed to parse key string: {}", kv.key);
146 let () = self.rm_key(&kv.key)?;
147 return Ok(None);
148 }
149 };
150 let key = match ClientIdentifier::from_str(key) {
151 Ok(v) => v,
152 Err(e) => {
153 warn!("client id from string conversion failed: {}", e);
154 let () = self.rm_key(&kv.key)?;
155 return Ok(None);
156 }
157 };
158 let val = match kv.val {
159 fidl_fuchsia_stash::Value::Stringval(v) => v,
160 v => {
161 warn!("invalid value variant stored in stash: {:?}", v);
162 let () = self.rm_key(&kv.key)?;
163 return Ok(None);
164 }
165 };
166 let val: LeaseRecord = match serde_json::from_str(&val) {
167 Ok(v) => v,
168 Err(e) => {
169 warn!("failed to parse JSON from string: {}", e);
170 let () = self.rm_key(&kv.key)?;
171 return Ok(None);
172 }
173 };
174 Ok(Some((key, val)))
175 })
176 .try_collect()
177 .await
178 }
179
180 pub async fn load_options(&self) -> Result<HashMap<OptionCode, DhcpOption>, StashError> {
182 let val = self.proxy.get_value(OPTIONS_KEY).await?;
183 let val = match val {
184 Some(v) => v,
185 None => return Ok(HashMap::new()),
186 };
187 match *val {
188 fidl_fuchsia_stash::Value::Stringval(v) => {
189 Ok(serde_json::from_str::<Vec<DhcpOption>>(&v)
190 .map_err(|e| StashError::JsonDeserialization(v.clone(), e))?
191 .into_iter()
192 .map(|opt| (opt.code(), opt))
193 .collect())
194 }
195 v => Err(StashError::UnexpectedStashValue {
196 actual: v,
197 expected: fidl_fuchsia_stash::Value::Stringval(String::new()),
198 }),
199 }
200 }
201
202 pub async fn load_parameters(&self) -> Result<ServerParameters, StashError> {
204 let val = self
205 .proxy
206 .get_value(PARAMETERS_KEY)
207 .await?
208 .ok_or(StashError::MissingValue(PARAMETERS_KEY.to_string()))?;
209 match *val {
210 fidl_fuchsia_stash::Value::Stringval(v) => Ok(serde_json::from_str(&v)
211 .map_err(|e| StashError::JsonDeserialization(v.clone(), e))?),
212 v => Err(StashError::UnexpectedStashValue {
213 actual: v,
214 expected: fidl_fuchsia_stash::Value::Stringval(String::new()),
215 }),
216 }
217 }
218
219 fn rm_key(&self, key: &str) -> Result<(), StashError> {
220 let () = self.proxy.delete_value(key)?;
221 let () = self.proxy.commit()?;
222 Ok(())
223 }
224
225 pub fn clear(&self) -> Result<(), StashError> {
229 let () = self.proxy.delete_prefix(&self.prefix)?;
230 let () = self.proxy.commit()?;
231 Ok(())
232 }
233
234 #[cfg(test)]
235 pub fn clone_proxy(&self) -> fidl_fuchsia_stash::StoreAccessorProxy {
236 self.proxy.clone()
237 }
238
239 pub(crate) fn client_key(&self, client_id: &ClientIdentifier) -> String {
240 format!("{}-{}", self.prefix, client_id)
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::configuration::{LeaseLength, ManagedAddresses};
248 use net_declare::net::prefix_length_v4;
249 use net_declare::std::ip_v4;
250
251 fn new_stash(test_prefix: &str) -> Result<(Stash, String), StashError> {
256 use rand::distributions::DistString as _;
257 let rand_id = rand::distributions::Alphanumeric.sample_string(&mut rand::thread_rng(), 8);
258 let stash = Stash::new_with_prefix(&rand_id, test_prefix)?;
259 let () = stash.proxy.delete_prefix(&stash.prefix)?;
261 let () = stash.proxy.commit()?;
262 Ok((stash, rand_id))
263 }
264
265 #[fuchsia_async::run_singlethreaded(test)]
266 async fn stash_new_with_prefix() {
267 assert_matches::assert_matches!(
268 Stash::new_with_prefix("stash_new", "valid"),
269 Ok(Stash { .. })
270 );
271 assert_matches::assert_matches!(
272 Stash::new_with_prefix("stash_new", "invalid-"),
273 Err(StashError::InvalidPrefix { .. })
274 );
275 assert_matches::assert_matches!(
276 Stash::new_with_prefix("stash_new", "invalid:"),
277 Err(StashError::InvalidPrefix { .. })
278 );
279 assert_matches::assert_matches!(
280 Stash::new_with_prefix("stash_new", ""),
281 Err(StashError::EmptyPrefix)
282 );
283 let () = match Stash::new_with_prefix("stash_new", "a-b-c-d") {
284 Err(StashError::InvalidPrefix { invalid_chars, prefix: _prefix }) => {
285 assert_eq!(invalid_chars, ['-'].iter().cloned().collect())
286 }
287 v => {
288 panic!("new_with_prefix returned {:?}, expected StashError::InvalidPrefix{{..}}", v)
289 }
290 };
291 let () = match Stash::new_with_prefix("stash_new", "a-b-c:d-e") {
292 Err(StashError::InvalidPrefix { invalid_chars, prefix: _prefix }) => {
293 assert_eq!(invalid_chars, ['-', ':'].iter().cloned().collect())
294 }
295 v => {
296 panic!("new_with_prefix returned {:?}, expected StashError::InvalidPrefix{{..}}", v)
297 }
298 };
299 }
300
301 #[fuchsia_async::run_singlethreaded(test)]
302 async fn store_client_succeeds() {
303 let (mut stash, id) =
304 new_stash("store_client_succeeds").expect("failed to create new stash");
305 let accessor_client = stash.proxy.clone();
306
307 let client_id = ClientIdentifier::from(crate::server::tests::random_mac_generator());
309 let client_record = LeaseRecord::default();
310 let () = stash
311 .insert(&client_id, &client_record)
312 .unwrap_or_else(|err| panic!("failed to store client in {}: {:?}", id, err));
313
314 let value = accessor_client
316 .get_value(&stash.client_key(&client_id))
317 .await
318 .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
319 let value = match *value.unwrap() {
320 fidl_fuchsia_stash::Value::Stringval(v) => v,
321 v => panic!("stored value is not a string: {:?}", v),
322 };
323 let value: LeaseRecord =
324 serde_json::from_str(&value).expect("failed to decode lease record");
325 assert_eq!(value, client_record);
326 }
327
328 #[fuchsia_async::run_singlethreaded(test)]
329 async fn store_options_succeeds() {
330 let (mut stash, id) = new_stash("store_options_succeeds").expect("failed to create stash");
331 let accessor_client = stash.proxy.clone();
332
333 let opts = vec![
334 DhcpOption::SubnetMask(prefix_length_v4!(24)),
335 DhcpOption::DomainNameServer([ip_v4!("1.2.3.4"), ip_v4!("4.3.2.1")].into()),
336 ];
337 let () = stash.store_options(&opts).expect("failed to store options in stash");
338 let value = accessor_client
339 .get_value(&OPTIONS_KEY.to_string())
340 .await
341 .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
342
343 let value = match *value.unwrap() {
344 fidl_fuchsia_stash::Value::Stringval(v) => v,
345 v => panic!("stored value is not a string: {:?}", v),
346 };
347 let value: Vec<DhcpOption> = serde_json::from_str(&value)
348 .unwrap_or_else(|err| panic!("failed to deserialize from {}: {:?}", value, err));
349 assert_eq!(value, opts);
350 }
351
352 #[fuchsia_async::run_singlethreaded(test)]
353 async fn store_parameters_succeeds() {
354 let (mut stash, id) =
355 new_stash("store_parameters_succeeds").expect("failed to create stash");
356 let accessor_client = stash.proxy.clone();
357
358 let params = ServerParameters {
359 server_ips: vec![ip_v4!("192.168.0.1")],
360 lease_length: LeaseLength { default_seconds: 42, max_seconds: 100 },
361 managed_addrs: ManagedAddresses {
362 mask: crate::configuration::SubnetMask::new(prefix_length_v4!(24)),
363 pool_range_start: ip_v4!("192.168.0.10"),
364 pool_range_stop: ip_v4!("192.168.0.254"),
365 },
366 permitted_macs: crate::configuration::PermittedMacs(Vec::new()),
367 static_assignments: crate::configuration::StaticAssignments(HashMap::new()),
368 arp_probe: false,
369 bound_device_names: vec![],
370 };
371 let () = stash.store_parameters(¶ms).expect("failed to store parameters");
372 let value = accessor_client
373 .get_value(&PARAMETERS_KEY.to_string())
374 .await
375 .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
376
377 let value = match *value.unwrap() {
378 fidl_fuchsia_stash::Value::Stringval(v) => v,
379 v => panic!("stored value is not a string: {:?}", v),
380 };
381 let value: ServerParameters = serde_json::from_str(&value)
382 .unwrap_or_else(|err| panic!("failed to deserialize from {}: {:?}", value, err));
383 assert_eq!(value, params);
384 }
385
386 #[fuchsia_async::run_singlethreaded(test)]
387 async fn load_clients_with_populated_stash_returns_cached_clients() {
388 let (stash, id) = new_stash("load_clients_with_populated_stash_returns_cached_clients")
389 .expect("failed to create stash");
390 let accessor = stash.proxy.clone();
391
392 let client_id = ClientIdentifier::from(crate::server::tests::random_mac_generator());
393 let client_record = LeaseRecord::default();
394 let serialized_client =
395 serde_json::to_string(&client_record).expect("serialization failed");
396 let client_key = stash.client_key(&client_id);
397 let client_val = fidl_fuchsia_stash::Value::Stringval(serialized_client);
398 let () = accessor
399 .set_value(&client_key, client_val)
400 .unwrap_or_else(|err| panic!("failed to set value in {}: {:?}", id, err));
401 let () = accessor.commit().unwrap_or_else(|err| {
402 panic!("failed to commit stash state change in {}: {:?}", id, err)
403 });
404
405 let loaded_cache = stash
406 .load_client_records()
407 .await
408 .unwrap_or_else(|err| panic!("failed to load map from stash in {}: {:?}", id, err));
409
410 let cached_clients = std::iter::once((client_id, client_record)).collect();
411 assert_eq!(loaded_cache, cached_clients);
412 }
413
414 #[fuchsia_async::run_singlethreaded(test)]
415 async fn load_options_with_stashed_options_returns_options() {
416 let (stash, id) = new_stash("load_options_with_stashed_options_returns_options")
417 .expect("failed to create stash");
418 let accessor = stash.proxy.clone();
419
420 let opts = vec![
421 DhcpOption::SubnetMask(prefix_length_v4!(24)),
422 DhcpOption::DomainNameServer([ip_v4!("1.2.3.4"), ip_v4!("4.3.2.1")].into()),
423 ];
424 let serialized_opts = serde_json::to_string(&opts).expect("serialization failed");
425 let opts = opts.into_iter().map(|o| (o.code(), o)).collect();
426 let () = accessor
427 .set_value(
428 &OPTIONS_KEY.to_string(),
429 fidl_fuchsia_stash::Value::Stringval(serialized_opts),
430 )
431 .unwrap_or_else(|err| {
432 panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
433 });
434 let () = accessor.commit().unwrap_or_else(|err| {
435 panic!("failed to commit stash state change in {}: {:?}", id, err)
436 });
437
438 let loaded_opts = stash.load_options().await.expect("failed to load options");
439 assert_eq!(loaded_opts, opts);
440 }
441
442 #[fuchsia_async::run_singlethreaded(test)]
443 async fn load_options_with_no_stashed_options_returns_empty_map() {
444 let (stash, _id) = new_stash("load_options_with_no_stashed_options_returns_empty_vec")
445 .expect("failed to create stash");
446
447 let opts = stash.load_options().await.expect("failed to load options");
448 assert_eq!(opts, HashMap::new());
449 }
450
451 #[fuchsia_async::run_singlethreaded(test)]
452 async fn load_parameters_with_stashed_parameters_returns_parameters() {
453 let (stash, id) = new_stash("load_parameters_with_stashed_parameters_returns_parameters")
454 .expect("faield to create stash");
455 let accessor = stash.proxy.clone();
456
457 let params = ServerParameters {
458 server_ips: vec![ip_v4!("192.168.0.1")],
459 lease_length: LeaseLength { default_seconds: 42, max_seconds: 100 },
460 managed_addrs: ManagedAddresses {
461 mask: crate::configuration::SubnetMask::new(prefix_length_v4!(24)),
462 pool_range_start: ip_v4!("192.168.0.10"),
463 pool_range_stop: ip_v4!("192.168.0.254"),
464 },
465 permitted_macs: crate::configuration::PermittedMacs(Vec::new()),
466 static_assignments: crate::configuration::StaticAssignments(HashMap::new()),
467 arp_probe: false,
468 bound_device_names: vec![],
469 };
470 let serialized_params = serde_json::to_string(¶ms).expect("serialization failed");
471 let () = accessor
472 .set_value(
473 &PARAMETERS_KEY.to_string(),
474 fidl_fuchsia_stash::Value::Stringval(serialized_params),
475 )
476 .unwrap_or_else(|err| {
477 panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
478 });
479 let () = accessor.commit().unwrap_or_else(|err| {
480 panic!("failed to commit stash state change in {}: {:?}", id, err)
481 });
482
483 let loaded_params = stash.load_parameters().await.expect("failed to load parameters");
484 assert_eq!(loaded_params, params);
485 }
486
487 #[fuchsia_async::run_singlethreaded(test)]
488 async fn load_parameters_with_no_stashed_parameters_returns_err() {
489 let (stash, _id) = new_stash("load_parameters_with_no_stashed_parameters_returns_err")
490 .expect("failed to create stash");
491 assert_matches::assert_matches!(
492 stash.load_parameters().await.expect_err("load_parameters should have returned err"),
493 StashError::MissingValue(String { .. })
494 );
495 }
496
497 #[fuchsia_async::run_singlethreaded(test)]
498 async fn load_clients_with_stash_containing_invalid_entries_returns_empty_cache() {
499 let (stash, id) =
500 new_stash("load_clients_with_stash_containing_invalid_entries_returns_empty_cache")
501 .expect("failed to create stash");
502 let accessor = stash.proxy.clone();
503
504 let client_id = ClientIdentifier::from(crate::server::tests::random_mac_generator());
505 let client_record = LeaseRecord::default();
506 let serialized_client =
507 serde_json::to_string(&client_record).expect("serialization failed");
508 let invalid_key = "invalid_key";
509 let client_stringval = fidl_fuchsia_stash::Value::Stringval(serialized_client);
510 let () = accessor.set_value(invalid_key, client_stringval).unwrap_or_else(|err| {
511 panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
512 });
513 let client_key = stash.client_key(&client_id);
514 let client_intval = fidl_fuchsia_stash::Value::Intval(42);
515 let () = accessor.set_value(&client_key, client_intval).unwrap_or_else(|err| {
516 panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
517 });
518 let () = accessor.commit().unwrap_or_else(|err| {
519 panic!("failed to commit stash state change in {}: {:?}", id, err)
520 });
521
522 let loaded_cache = stash
523 .load_client_records()
524 .await
525 .unwrap_or_else(|err| panic!("failed to load map from stash in {}: {:?}", id, err));
526
527 let empty_cache = HashMap::new();
528 assert_eq!(loaded_cache, empty_cache);
529 }
530
531 #[fuchsia_async::run_singlethreaded(test)]
532 async fn delete_client_succeeds() {
533 let (mut stash, id) = new_stash("delete_client_succeeds").expect("failed to create stash");
534 let accessor = stash.proxy.clone();
535
536 let client_id = ClientIdentifier::from(crate::server::tests::random_mac_generator());
538 let client_record = LeaseRecord::default();
539 let () = stash
540 .insert(&client_id, &client_record)
541 .unwrap_or_else(|err| panic!("failed to store client in {}: {:?}", id, err));
542
543 let client_key = stash.client_key(&client_id);
545 let value = accessor
546 .get_value(&client_key)
547 .await
548 .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
549 assert!(value.is_some());
550
551 let () = stash
553 .delete(&client_id)
554 .unwrap_or_else(|err| panic!("failed to delete client in {}: {:?}", id, err));
555 let value = accessor
556 .get_value(&client_key)
557 .await
558 .unwrap_or_else(|err| panic!("failed to get value from {}: {:?}", id, err));
559 assert!(value.is_none());
560 }
561
562 #[fuchsia_async::run_singlethreaded(test)]
563 async fn clear_with_populated_stash_clears_stash() {
564 let (stash, id) =
565 new_stash("clear_with_populated_stash_clears_stash").expect("failed to create stash");
566 let accessor = stash.proxy.clone();
567
568 let client_mac = ClientIdentifier::from(crate::server::tests::random_mac_generator());
570 let client_record = LeaseRecord::default();
571 let serialized_client =
572 serde_json::to_string(&client_record).expect("serialization failed");
573 let client_key = stash.client_key(&client_mac);
574 let client_val = fidl_fuchsia_stash::Value::Stringval(serialized_client);
575 let () = accessor.set_value(&client_key, client_val).unwrap_or_else(|err| {
576 panic!("failed to set value in stash for key={}: {:?}", OPTIONS_KEY, err)
577 });
578 let () = accessor.commit().unwrap_or_else(|err| {
579 panic!("failed to commit stash state change in {}: {:?}", id, err)
580 });
581
582 let () = stash
584 .clear()
585 .unwrap_or_else(|err| panic!("failed to clear stash in {}: {:?}", id, err));
586
587 let (iter, server) =
589 fidl::endpoints::create_proxy::<fidl_fuchsia_stash::GetIteratorMarker>();
590 let () = accessor.get_prefix(&stash.prefix, server).expect("failed to get prefix iterator");
591 let stash_contents = iter.get_next().await.expect("failed to get next item for iterator");
592 assert_eq!(stash_contents.len(), 0);
593 }
594}