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