1use anyhow::{Context as _, Error};
6use component_debug::dirs::{OpenDirType, connect_to_instance_protocol};
7use fidl_fuchsia_net_stackmigrationdeprecated as fnet_stack_migration;
8use once_cell::sync::OnceCell;
9use serde::Serialize;
10use serde_json::Value;
11
12fn serialize_ipv4<S: serde::Serializer>(
13 addresses: &Vec<std::net::Ipv4Addr>,
14 serializer: S,
15) -> Result<S::Ok, S::Error> {
16 serializer.collect_seq(addresses.iter().map(|address| address.octets()))
17}
18
19fn serialize_ipv6<S: serde::Serializer>(
20 addresses: &Vec<std::net::Ipv6Addr>,
21 serializer: S,
22) -> Result<S::Ok, S::Error> {
23 serializer.collect_seq(addresses.iter().map(|address| address.octets()))
24}
25
26fn serialize_mac<S: serde::Serializer>(
27 mac: &Option<fidl_fuchsia_net_ext::MacAddress>,
28 serializer: S,
29) -> Result<S::Ok, S::Error> {
30 match mac {
31 None => serializer.serialize_none(),
32 Some(fidl_fuchsia_net_ext::MacAddress { octets }) => serializer.collect_seq(octets.iter()),
33 }
34}
35
36#[derive(Serialize)]
37enum DeviceClass {
38 Loopback,
39 Blackhole,
40 Virtual,
41 Ethernet,
42 WlanClient,
43 Ppp,
44 Bridge,
45 WlanAp,
46 Lowpan,
47}
48
49#[derive(Serialize)]
50pub struct Properties {
51 id: u64,
52 name: String,
53 device_class: DeviceClass,
54 online: bool,
55 #[serde(serialize_with = "serialize_ipv4")]
56 ipv4_addresses: Vec<std::net::Ipv4Addr>,
57 #[serde(serialize_with = "serialize_ipv6")]
58 ipv6_addresses: Vec<std::net::Ipv6Addr>,
59 #[serde(serialize_with = "serialize_mac")]
60 mac: Option<fidl_fuchsia_net_ext::MacAddress>,
61}
62
63impl
64 From<(
65 fidl_fuchsia_net_interfaces_ext::Properties<fidl_fuchsia_net_interfaces_ext::AllInterest>,
66 Option<fidl_fuchsia_net::MacAddress>,
67 )> for Properties
68{
69 fn from(
70 t: (
71 fidl_fuchsia_net_interfaces_ext::Properties<
72 fidl_fuchsia_net_interfaces_ext::AllInterest,
73 >,
74 Option<fidl_fuchsia_net::MacAddress>,
75 ),
76 ) -> Self {
77 use itertools::Itertools as _;
78
79 let (
80 fidl_fuchsia_net_interfaces_ext::Properties {
81 id,
82 name,
83 port_class,
84 online,
85 addresses,
86 has_default_ipv4_route: _,
87 has_default_ipv6_route: _,
88 port_identity_koid: _,
89 },
90 mac,
91 ) = t;
92 let device_class = match port_class {
93 fidl_fuchsia_net_interfaces_ext::PortClass::Loopback => DeviceClass::Loopback,
94 fidl_fuchsia_net_interfaces_ext::PortClass::Blackhole => DeviceClass::Blackhole,
95 fidl_fuchsia_net_interfaces_ext::PortClass::Virtual => DeviceClass::Virtual,
96 fidl_fuchsia_net_interfaces_ext::PortClass::Ethernet => DeviceClass::Ethernet,
97 fidl_fuchsia_net_interfaces_ext::PortClass::WlanClient => DeviceClass::WlanClient,
98 fidl_fuchsia_net_interfaces_ext::PortClass::WlanAp => DeviceClass::WlanAp,
99 fidl_fuchsia_net_interfaces_ext::PortClass::Ppp => DeviceClass::Ppp,
100 fidl_fuchsia_net_interfaces_ext::PortClass::Bridge => DeviceClass::Bridge,
101 fidl_fuchsia_net_interfaces_ext::PortClass::Lowpan => DeviceClass::Lowpan,
102 };
103 let (ipv4_addresses, ipv6_addresses) =
104 addresses.into_iter().partition_map::<_, _, _, std::net::Ipv4Addr, std::net::Ipv6Addr>(
105 |fidl_fuchsia_net_interfaces_ext::Address {
106 addr,
107 valid_until: _,
108 preferred_lifetime_info: _,
109 assignment_state,
110 }| {
111 assert_eq!(
113 assignment_state,
114 fidl_fuchsia_net_interfaces::AddressAssignmentState::Assigned,
115 "Support for unassigned addresses have not been implemented",
116 );
117 let fidl_fuchsia_net_ext::Subnet { addr, prefix_len: _ } = addr.into();
118 let fidl_fuchsia_net_ext::IpAddress(addr) = addr;
119 match addr {
120 std::net::IpAddr::V4(addr) => itertools::Either::Left(addr),
121 std::net::IpAddr::V6(addr) => itertools::Either::Right(addr),
122 }
123 },
124 );
125 Self {
126 id: id.get(),
127 name,
128 device_class,
129 online,
130 ipv4_addresses,
131 ipv6_addresses,
132 mac: mac.map(Into::into),
133 }
134 }
135}
136
137#[derive(Debug, PartialEq, Serialize)]
138pub enum NetstackVersion {
140 Netstack2,
141 Netstack3,
142}
143
144impl From<fnet_stack_migration::NetstackVersion> for NetstackVersion {
145 fn from(version: fnet_stack_migration::NetstackVersion) -> NetstackVersion {
146 match version {
147 fnet_stack_migration::NetstackVersion::Netstack2 => NetstackVersion::Netstack2,
148 fnet_stack_migration::NetstackVersion::Netstack3 => NetstackVersion::Netstack3,
149 }
150 }
151}
152impl From<NetstackVersion> for fnet_stack_migration::NetstackVersion {
153 fn from(version: NetstackVersion) -> fnet_stack_migration::NetstackVersion {
154 match version {
155 NetstackVersion::Netstack2 => fnet_stack_migration::NetstackVersion::Netstack2,
156 NetstackVersion::Netstack3 => fnet_stack_migration::NetstackVersion::Netstack3,
157 }
158 }
159}
160
161impl From<fnet_stack_migration::VersionSetting> for NetstackVersion {
162 fn from(version: fnet_stack_migration::VersionSetting) -> NetstackVersion {
163 let fnet_stack_migration::VersionSetting { version } = version;
164 version.into()
165 }
166}
167
168impl From<NetstackVersion> for fnet_stack_migration::VersionSetting {
169 fn from(version: NetstackVersion) -> fnet_stack_migration::VersionSetting {
170 fnet_stack_migration::VersionSetting { version: version.into() }
171 }
172}
173
174impl TryFrom<Value> for NetstackVersion {
175 type Error = Error;
176 fn try_from(value: Value) -> Result<NetstackVersion, Error> {
177 match value {
178 Value::String(value) => match value.to_lowercase().as_str() {
179 "ns2" | "netstack2" => Ok(NetstackVersion::Netstack2),
180 "ns3" | "netstack3" => Ok(NetstackVersion::Netstack3),
181 _ => Err(anyhow!("unrecognized netstack version: {}", value)),
182 },
183 _ => Err(anyhow!("unrecognized netstack version: {:?}", value)),
184 }
185 }
186}
187
188#[derive(Serialize)]
189pub struct InEffectNetstackVersion {
191 current_boot: NetstackVersion,
192 automated_selection: Option<NetstackVersion>,
193 user_selection: Option<NetstackVersion>,
194}
195
196impl From<fnet_stack_migration::InEffectVersion> for InEffectNetstackVersion {
197 fn from(in_effect: fnet_stack_migration::InEffectVersion) -> InEffectNetstackVersion {
198 let fnet_stack_migration::InEffectVersion { current_boot, automated, user } = in_effect;
199 InEffectNetstackVersion {
200 current_boot: current_boot.into(),
201 automated_selection: automated.map(|selection| (*selection).into()),
202 user_selection: user.map(|selection| (*selection).into()),
203 }
204 }
205}
206
207#[derive(Debug, Default)]
209pub struct NetstackFacade {
210 interfaces_state: OnceCell<fidl_fuchsia_net_interfaces::StateProxy>,
211 root_interfaces: OnceCell<fidl_fuchsia_net_root::InterfacesProxy>,
212 netstack_migration_state: OnceCell<fnet_stack_migration::StateProxy>,
213 netstack_migration_control: OnceCell<fnet_stack_migration::ControlProxy>,
214}
215
216async fn get_netstack_proxy<P: fidl::endpoints::DiscoverableProtocolMarker>()
217-> Result<P::Proxy, Error> {
218 let query =
219 fuchsia_component::client::connect_to_protocol::<fidl_fuchsia_sys2::RealmQueryMarker>()?;
220 let moniker = "./core/network/netstack".try_into()?;
221 let proxy = connect_to_instance_protocol::<P>(&moniker, OpenDirType::Exposed, &query).await?;
222 Ok(proxy)
223}
224
225async fn get_netstack_migration_proxy<P: fidl::endpoints::DiscoverableProtocolMarker>()
226-> Result<P::Proxy, Error> {
227 let query =
228 fuchsia_component::client::connect_to_protocol::<fidl_fuchsia_sys2::RealmQueryMarker>()?;
229 let moniker = "./core/network/netstack-migration".try_into()?;
230 let proxy = connect_to_instance_protocol::<P>(&moniker, OpenDirType::Exposed, &query).await?;
231 Ok(proxy)
232}
233
234impl NetstackFacade {
235 async fn get_interfaces_state(
236 &self,
237 ) -> Result<&fidl_fuchsia_net_interfaces::StateProxy, Error> {
238 let Self {
239 interfaces_state,
240 root_interfaces: _,
241 netstack_migration_state: _,
242 netstack_migration_control: _,
243 } = self;
244 if let Some(state_proxy) = interfaces_state.get() {
245 Ok(state_proxy)
246 } else {
247 let state_proxy =
248 get_netstack_proxy::<fidl_fuchsia_net_interfaces::StateMarker>().await?;
249 interfaces_state.set(state_proxy).unwrap();
250 let state_proxy = interfaces_state.get().unwrap();
251 Ok(state_proxy)
252 }
253 }
254
255 async fn get_root_interfaces(&self) -> Result<&fidl_fuchsia_net_root::InterfacesProxy, Error> {
256 let Self {
257 interfaces_state: _,
258 root_interfaces,
259 netstack_migration_state: _,
260 netstack_migration_control: _,
261 } = self;
262 if let Some(interfaces_proxy) = root_interfaces.get() {
263 Ok(interfaces_proxy)
264 } else {
265 let interfaces_proxy =
266 get_netstack_proxy::<fidl_fuchsia_net_root::InterfacesMarker>().await?;
267 root_interfaces.set(interfaces_proxy).unwrap();
268 let interfaces_proxy = root_interfaces.get().unwrap();
269 Ok(interfaces_proxy)
270 }
271 }
272
273 async fn get_netstack_migration_state(
274 &self,
275 ) -> Result<&fnet_stack_migration::StateProxy, Error> {
276 let Self {
277 interfaces_state: _,
278 root_interfaces: _,
279 netstack_migration_state,
280 netstack_migration_control: _,
281 } = self;
282 if let Some(state_proxy) = netstack_migration_state.get() {
283 Ok(state_proxy)
284 } else {
285 let state_proxy =
286 get_netstack_migration_proxy::<fnet_stack_migration::StateMarker>().await?;
287 netstack_migration_state.set(state_proxy).unwrap();
288 let state_proxy = netstack_migration_state.get().unwrap();
289 Ok(state_proxy)
290 }
291 }
292
293 async fn get_netstack_migration_control(
294 &self,
295 ) -> Result<&fnet_stack_migration::ControlProxy, Error> {
296 let Self {
297 interfaces_state: _,
298 root_interfaces: _,
299 netstack_migration_state: _,
300 netstack_migration_control,
301 } = self;
302 if let Some(control_proxy) = netstack_migration_control.get() {
303 Ok(control_proxy)
304 } else {
305 let control_proxy =
306 get_netstack_migration_proxy::<fnet_stack_migration::ControlMarker>().await?;
307 netstack_migration_control.set(control_proxy).unwrap();
308 let control_proxy = netstack_migration_control.get().unwrap();
309 Ok(control_proxy)
310 }
311 }
312
313 async fn get_control(
314 &self,
315 id: u64,
316 ) -> Result<fidl_fuchsia_net_interfaces_ext::admin::Control, Error> {
317 let root_interfaces = self.get_root_interfaces().await?;
318 let (control, server_end) =
319 fidl_fuchsia_net_interfaces_ext::admin::Control::create_endpoints()
320 .context("create admin control endpoints")?;
321 let () = root_interfaces.get_admin(id, server_end).context("send get admin request")?;
322 Ok(control)
323 }
324
325 pub async fn enable_interface(&self, id: u64) -> Result<(), Error> {
326 let control = self.get_control(id).await?;
327 let _did_enable: bool = control
328 .enable()
329 .await
330 .map_err(anyhow::Error::new)
331 .and_then(|res| {
332 res.map_err(|e: fidl_fuchsia_net_interfaces_admin::ControlEnableError| {
333 anyhow::anyhow!("{:?}", e)
334 })
335 })
336 .with_context(|| format!("failed to enable interface {}", id))?;
337 Ok(())
338 }
339
340 pub async fn disable_interface(&self, id: u64) -> Result<(), Error> {
341 let control = self.get_control(id).await?;
342 let _did_disable: bool = control
343 .disable()
344 .await
345 .map_err(anyhow::Error::new)
346 .and_then(|res| {
347 res.map_err(|e: fidl_fuchsia_net_interfaces_admin::ControlDisableError| {
348 anyhow::anyhow!("{:?}", e)
349 })
350 })
351 .with_context(|| format!("failed to disable interface {}", id))?;
352 Ok(())
353 }
354
355 pub async fn list_interfaces(&self) -> Result<Vec<Properties>, Error> {
356 let interfaces_state = self.get_interfaces_state().await?;
357 let root_interfaces = self.get_root_interfaces().await?;
358 let stream = fidl_fuchsia_net_interfaces_ext::event_stream_from_state(
361 interfaces_state,
362 fidl_fuchsia_net_interfaces_ext::IncludedAddresses::OnlyAssigned,
363 )?;
364 let response = fidl_fuchsia_net_interfaces_ext::existing(
365 stream,
366 std::collections::HashMap::<u64, _>::new(),
367 )
368 .await?;
369 let response = response.into_values().map(
370 |fidl_fuchsia_net_interfaces_ext::PropertiesAndState { properties, state: () }| async {
371 match root_interfaces.get_mac(properties.id.get()).await? {
372 Ok(mac) => {
373 let mac = mac.map(|boxed_mac| *boxed_mac);
374 let view: Properties = (properties, mac).into();
375 Ok::<_, Error>(Some(view))
376 }
377 Err(fidl_fuchsia_net_root::InterfacesGetMacError::NotFound) => {
378 Ok::<_, Error>(None)
381 }
382 }
383 },
384 );
385 let mut response: Vec<Properties> =
386 futures::future::try_join_all(response).await?.into_iter().filter_map(|r| r).collect();
387 let () = response.sort_by_key(|&Properties { id, .. }| id);
388 Ok(response)
389 }
390
391 async fn get_addresses<T, F: Copy + FnMut(fidl_fuchsia_net::Subnet) -> Option<T>>(
392 &self,
393 f: F,
394 ) -> Result<Vec<T>, Error> {
395 let mut output = Vec::new();
396
397 let interfaces_state = self.get_interfaces_state().await?;
398 let (watcher, server) =
399 fidl::endpoints::create_proxy::<fidl_fuchsia_net_interfaces::WatcherMarker>();
400 let () = interfaces_state
401 .get_watcher(&fidl_fuchsia_net_interfaces::WatcherOptions::default(), server)?;
402
403 loop {
404 match watcher.watch().await? {
405 fidl_fuchsia_net_interfaces::Event::Existing(
406 fidl_fuchsia_net_interfaces::Properties { addresses, .. },
407 ) => {
408 let addresses = addresses.unwrap();
409 let () = output.extend(
410 addresses
411 .into_iter()
412 .map(
413 |fidl_fuchsia_net_interfaces::Address {
414 addr,
415 valid_until: _,
416 ..
417 }| addr.unwrap(),
418 )
419 .filter_map(f),
420 );
421 }
422 fidl_fuchsia_net_interfaces::Event::Idle(fidl_fuchsia_net_interfaces::Empty {}) => {
423 break;
424 }
425 event => unreachable!("{:?}", event),
426 }
427 }
428
429 Ok(output)
430 }
431
432 pub fn get_ipv6_addresses(
433 &self,
434 ) -> impl std::future::Future<Output = Result<Vec<std::net::Ipv6Addr>, Error>> + '_ {
435 self.get_addresses(|addr| {
436 let fidl_fuchsia_net_ext::Subnet { addr, prefix_len: _ } = addr.into();
437 let fidl_fuchsia_net_ext::IpAddress(addr) = addr;
438 match addr {
439 std::net::IpAddr::V4(_) => None,
440 std::net::IpAddr::V6(addr) => Some(addr),
441 }
442 })
443 }
444
445 pub fn get_link_local_ipv6_addresses(
446 &self,
447 ) -> impl std::future::Future<Output = Result<Vec<std::net::Ipv6Addr>, Error>> + '_ {
448 use futures::TryFutureExt as _;
449
450 self.get_ipv6_addresses().map_ok(|addresses| {
451 addresses.into_iter().filter(|address| address.octets()[..2] == [0xfe, 0x80]).collect()
452 })
453 }
454
455 pub async fn get_netstack_version(&self) -> Result<InEffectNetstackVersion, Error> {
460 let netstack_migration_state = self.get_netstack_migration_state().await?;
461 Ok(netstack_migration_state.get_netstack_version().await?.into())
462 }
463
464 pub async fn set_user_netstack_version(&self, version: NetstackVersion) -> Result<(), Error> {
469 let netstack_migration_control = self.get_netstack_migration_control().await?;
470 Ok(netstack_migration_control.set_user_netstack_version(Some(&version.into())).await?)
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477 use assert_matches::assert_matches;
478 use futures::StreamExt as _;
479 use test_case::test_case;
480 use {
481 fidl_fuchsia_net as fnet, fidl_fuchsia_net_interfaces as finterfaces,
482 fuchsia_async as fasync,
483 };
484
485 struct MockStateTester {
486 expected_state: Vec<Box<dyn FnOnce(finterfaces::WatcherRequest) + Send + 'static>>,
487 }
488
489 impl MockStateTester {
490 fn new() -> Self {
491 Self { expected_state: vec![] }
492 }
493
494 pub fn create_facade_and_serve_state(
495 self,
496 ) -> (NetstackFacade, impl std::future::Future<Output = ()>) {
497 let (interfaces_state, stream_future) = self.build_state_and_watcher();
498 (
499 NetstackFacade { interfaces_state: interfaces_state.into(), ..Default::default() },
500 stream_future,
501 )
502 }
503
504 fn push_state(
505 mut self,
506 request: impl FnOnce(finterfaces::WatcherRequest) + Send + 'static,
507 ) -> Self {
508 self.expected_state.push(Box::new(request));
509 self
510 }
511
512 fn build_state_and_watcher(
513 self,
514 ) -> (finterfaces::StateProxy, impl std::future::Future<Output = ()>) {
515 let (proxy, mut stream) =
516 fidl::endpoints::create_proxy_and_stream::<finterfaces::StateMarker>();
517 let stream_fut = async move {
518 match stream.next().await {
519 Some(Ok(finterfaces::StateRequest::GetWatcher { watcher, .. })) => {
520 let mut into_stream = watcher.into_stream();
521 for expected in self.expected_state {
522 let () = expected(into_stream.next().await.unwrap().unwrap());
523 }
524 let finterfaces::WatcherRequest::Watch { responder } =
525 into_stream.next().await.unwrap().unwrap();
526 let () = responder
527 .send(&finterfaces::Event::Idle(finterfaces::Empty {}))
528 .unwrap();
529 }
530 err => panic!("Error in request handler: {:?}", err),
531 }
532 };
533 (proxy, stream_fut)
534 }
535
536 fn expect_get_ipv6_addresses(self, result: Vec<fnet::Subnet>) -> Self {
537 let addresses = result
538 .into_iter()
539 .map(|addr| finterfaces::Address { addr: Some(addr), ..Default::default() })
540 .collect();
541 self.push_state(move |req| match req {
542 finterfaces::WatcherRequest::Watch { responder } => responder
543 .send(&finterfaces::Event::Existing(finterfaces::Properties {
544 addresses: Some(addresses),
545 ..Default::default()
546 }))
547 .unwrap(),
548 })
549 }
550 }
551
552 #[fasync::run_singlethreaded(test)]
553 async fn test_get_ipv6_addresses() {
554 let ipv6_octets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
555
556 let ipv6_address = fnet::Subnet {
557 addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr: ipv6_octets }),
558 prefix_len: 137,
560 };
561 let ipv4_address = fnet::Subnet {
562 addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [0, 1, 2, 3] }),
563 prefix_len: 139,
565 };
566 let all_addresses = [ipv6_address.clone(), ipv4_address.clone()];
567 let (facade, stream_fut) = MockStateTester::new()
568 .expect_get_ipv6_addresses(all_addresses.to_vec())
569 .create_facade_and_serve_state();
570 let facade_fut = async move {
571 let result_address: Vec<_> = facade.get_ipv6_addresses().await.unwrap();
572 assert_eq!(result_address, [std::net::Ipv6Addr::from(ipv6_octets)]);
573 };
574 futures::future::join(facade_fut, stream_fut).await;
575 }
576
577 #[fasync::run_singlethreaded(test)]
578 async fn test_get_link_local_ipv6_addresses() {
579 let ipv6_address = fnet::Subnet {
580 addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address {
581 addr: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
582 }),
583 prefix_len: 137,
585 };
586 let link_local_ipv6_octets = [0xfe, 0x80, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
587 let link_local_ipv6_address = fnet::Subnet {
588 addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr: link_local_ipv6_octets }),
589 prefix_len: 139,
591 };
592 let ipv4_address = fnet::Subnet {
593 addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [0, 1, 2, 3] }),
594 prefix_len: 141,
596 };
597 let all_addresses =
598 [ipv6_address.clone(), link_local_ipv6_address.clone(), ipv4_address.clone()];
599 let (facade, stream_fut) = MockStateTester::new()
600 .expect_get_ipv6_addresses(all_addresses.to_vec())
601 .create_facade_and_serve_state();
602 let facade_fut = async move {
603 let result_address: Vec<_> = facade.get_link_local_ipv6_addresses().await.unwrap();
604 assert_eq!(result_address, [std::net::Ipv6Addr::from(link_local_ipv6_octets)]);
605 };
606 futures::future::join(facade_fut, stream_fut).await;
607 }
608
609 #[test_case(Value::String("ns2".to_string()), Some(NetstackVersion::Netstack2); "ns2")]
610 #[test_case(Value::String("ns3".to_string()), Some(NetstackVersion::Netstack3); "ns3")]
611 #[test_case(Value::String("netstack2".to_string()), Some(NetstackVersion::Netstack2);
612 "netstack2")]
613 #[test_case(Value::String("netstack3".to_string()), Some(NetstackVersion::Netstack3);
614 "netstack3")]
615 #[test_case(Value::String("invalid".to_string()), None; "invalid_string")]
616 #[test_case(Value::Bool(false), None; "invalid_value")]
617 fn test_convert_netstack_version_from_json_value(
618 json_value: Value,
619 expected_version: Option<NetstackVersion>,
620 ) {
621 let version: Result<NetstackVersion, Error> = json_value.try_into();
622 match expected_version {
623 Some(expected_version) => assert_eq!(version.expect("parse version"), expected_version),
624 None => assert_matches!(version, Err(_)),
625 }
626 }
627
628 #[test_case(fnet_stack_migration::NetstackVersion::Netstack2, NetstackVersion::Netstack2;
629 "netstack2")]
630 #[test_case(fnet_stack_migration::NetstackVersion::Netstack3, NetstackVersion::Netstack3;
631 "netstack3")]
632 fn test_convert_netstack_version_from_fidl(
633 fidl_version: fnet_stack_migration::NetstackVersion,
634 expected_version: NetstackVersion,
635 ) {
636 assert_eq!(NetstackVersion::from(fidl_version), expected_version);
637 assert_eq!(
638 NetstackVersion::from(fnet_stack_migration::VersionSetting { version: fidl_version }),
639 expected_version
640 );
641 }
642}