1use anyhow::{Context as _, Error};
6use component_debug::dirs::{connect_to_instance_protocol, 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 = connect_to_instance_protocol::<P>(&moniker, OpenDirType::Exposed, &query).await?;
221 Ok(proxy)
222}
223
224async fn get_netstack_migration_proxy<P: fidl::endpoints::DiscoverableProtocolMarker>(
225) -> Result<P::Proxy, Error> {
226 let query =
227 fuchsia_component::client::connect_to_protocol::<fidl_fuchsia_sys2::RealmQueryMarker>()?;
228 let moniker = "./core/network/netstack-migration".try_into()?;
229 let proxy = connect_to_instance_protocol::<P>(&moniker, OpenDirType::Exposed, &query).await?;
230 Ok(proxy)
231}
232
233impl NetstackFacade {
234 async fn get_interfaces_state(
235 &self,
236 ) -> Result<&fidl_fuchsia_net_interfaces::StateProxy, Error> {
237 let Self {
238 interfaces_state,
239 root_interfaces: _,
240 netstack_migration_state: _,
241 netstack_migration_control: _,
242 } = self;
243 if let Some(state_proxy) = interfaces_state.get() {
244 Ok(state_proxy)
245 } else {
246 let state_proxy =
247 get_netstack_proxy::<fidl_fuchsia_net_interfaces::StateMarker>().await?;
248 interfaces_state.set(state_proxy).unwrap();
249 let state_proxy = interfaces_state.get().unwrap();
250 Ok(state_proxy)
251 }
252 }
253
254 async fn get_root_interfaces(&self) -> Result<&fidl_fuchsia_net_root::InterfacesProxy, Error> {
255 let Self {
256 interfaces_state: _,
257 root_interfaces,
258 netstack_migration_state: _,
259 netstack_migration_control: _,
260 } = self;
261 if let Some(interfaces_proxy) = root_interfaces.get() {
262 Ok(interfaces_proxy)
263 } else {
264 let interfaces_proxy =
265 get_netstack_proxy::<fidl_fuchsia_net_root::InterfacesMarker>().await?;
266 root_interfaces.set(interfaces_proxy).unwrap();
267 let interfaces_proxy = root_interfaces.get().unwrap();
268 Ok(interfaces_proxy)
269 }
270 }
271
272 async fn get_netstack_migration_state(
273 &self,
274 ) -> Result<&fnet_stack_migration::StateProxy, Error> {
275 let Self {
276 interfaces_state: _,
277 root_interfaces: _,
278 netstack_migration_state,
279 netstack_migration_control: _,
280 } = self;
281 if let Some(state_proxy) = netstack_migration_state.get() {
282 Ok(state_proxy)
283 } else {
284 let state_proxy =
285 get_netstack_migration_proxy::<fnet_stack_migration::StateMarker>().await?;
286 netstack_migration_state.set(state_proxy).unwrap();
287 let state_proxy = netstack_migration_state.get().unwrap();
288 Ok(state_proxy)
289 }
290 }
291
292 async fn get_netstack_migration_control(
293 &self,
294 ) -> Result<&fnet_stack_migration::ControlProxy, Error> {
295 let Self {
296 interfaces_state: _,
297 root_interfaces: _,
298 netstack_migration_state: _,
299 netstack_migration_control,
300 } = self;
301 if let Some(control_proxy) = netstack_migration_control.get() {
302 Ok(control_proxy)
303 } else {
304 let control_proxy =
305 get_netstack_migration_proxy::<fnet_stack_migration::ControlMarker>().await?;
306 netstack_migration_control.set(control_proxy).unwrap();
307 let control_proxy = netstack_migration_control.get().unwrap();
308 Ok(control_proxy)
309 }
310 }
311
312 async fn get_control(
313 &self,
314 id: u64,
315 ) -> Result<fidl_fuchsia_net_interfaces_ext::admin::Control, Error> {
316 let root_interfaces = self.get_root_interfaces().await?;
317 let (control, server_end) =
318 fidl_fuchsia_net_interfaces_ext::admin::Control::create_endpoints()
319 .context("create admin control endpoints")?;
320 let () = root_interfaces.get_admin(id, server_end).context("send get admin request")?;
321 Ok(control)
322 }
323
324 pub async fn enable_interface(&self, id: u64) -> Result<(), Error> {
325 let control = self.get_control(id).await?;
326 let _did_enable: bool = control
327 .enable()
328 .await
329 .map_err(anyhow::Error::new)
330 .and_then(|res| {
331 res.map_err(|e: fidl_fuchsia_net_interfaces_admin::ControlEnableError| {
332 anyhow::anyhow!("{:?}", e)
333 })
334 })
335 .with_context(|| format!("failed to enable interface {}", id))?;
336 Ok(())
337 }
338
339 pub async fn disable_interface(&self, id: u64) -> Result<(), Error> {
340 let control = self.get_control(id).await?;
341 let _did_disable: bool = control
342 .disable()
343 .await
344 .map_err(anyhow::Error::new)
345 .and_then(|res| {
346 res.map_err(|e: fidl_fuchsia_net_interfaces_admin::ControlDisableError| {
347 anyhow::anyhow!("{:?}", e)
348 })
349 })
350 .with_context(|| format!("failed to disable interface {}", id))?;
351 Ok(())
352 }
353
354 pub async fn list_interfaces(&self) -> Result<Vec<Properties>, Error> {
355 let interfaces_state = self.get_interfaces_state().await?;
356 let root_interfaces = self.get_root_interfaces().await?;
357 let stream = fidl_fuchsia_net_interfaces_ext::event_stream_from_state(
360 interfaces_state,
361 fidl_fuchsia_net_interfaces_ext::IncludedAddresses::OnlyAssigned,
362 )?;
363 let response = fidl_fuchsia_net_interfaces_ext::existing(
364 stream,
365 std::collections::HashMap::<u64, _>::new(),
366 )
367 .await?;
368 let response = response.into_values().map(
369 |fidl_fuchsia_net_interfaces_ext::PropertiesAndState { properties, state: () }| async {
370 match root_interfaces.get_mac(properties.id.get()).await? {
371 Ok(mac) => {
372 let mac = mac.map(|boxed_mac| *boxed_mac);
373 let view: Properties = (properties, mac).into();
374 Ok::<_, Error>(Some(view))
375 }
376 Err(fidl_fuchsia_net_root::InterfacesGetMacError::NotFound) => {
377 Ok::<_, Error>(None)
380 }
381 }
382 },
383 );
384 let mut response: Vec<Properties> =
385 futures::future::try_join_all(response).await?.into_iter().filter_map(|r| r).collect();
386 let () = response.sort_by_key(|&Properties { id, .. }| id);
387 Ok(response)
388 }
389
390 async fn get_addresses<T, F: Copy + FnMut(fidl_fuchsia_net::Subnet) -> Option<T>>(
391 &self,
392 f: F,
393 ) -> Result<Vec<T>, Error> {
394 let mut output = Vec::new();
395
396 let interfaces_state = self.get_interfaces_state().await?;
397 let (watcher, server) =
398 fidl::endpoints::create_proxy::<fidl_fuchsia_net_interfaces::WatcherMarker>();
399 let () = interfaces_state
400 .get_watcher(&fidl_fuchsia_net_interfaces::WatcherOptions::default(), server)?;
401
402 loop {
403 match watcher.watch().await? {
404 fidl_fuchsia_net_interfaces::Event::Existing(
405 fidl_fuchsia_net_interfaces::Properties { addresses, .. },
406 ) => {
407 let addresses = addresses.unwrap();
408 let () = output.extend(
409 addresses
410 .into_iter()
411 .map(
412 |fidl_fuchsia_net_interfaces::Address {
413 addr,
414 valid_until: _,
415 ..
416 }| addr.unwrap(),
417 )
418 .filter_map(f),
419 );
420 }
421 fidl_fuchsia_net_interfaces::Event::Idle(fidl_fuchsia_net_interfaces::Empty {}) => {
422 break
423 }
424 event => unreachable!("{:?}", event),
425 }
426 }
427
428 Ok(output)
429 }
430
431 pub fn get_ipv6_addresses(
432 &self,
433 ) -> impl std::future::Future<Output = Result<Vec<std::net::Ipv6Addr>, Error>> + '_ {
434 self.get_addresses(|addr| {
435 let fidl_fuchsia_net_ext::Subnet { addr, prefix_len: _ } = addr.into();
436 let fidl_fuchsia_net_ext::IpAddress(addr) = addr;
437 match addr {
438 std::net::IpAddr::V4(_) => None,
439 std::net::IpAddr::V6(addr) => Some(addr),
440 }
441 })
442 }
443
444 pub fn get_link_local_ipv6_addresses(
445 &self,
446 ) -> impl std::future::Future<Output = Result<Vec<std::net::Ipv6Addr>, Error>> + '_ {
447 use futures::TryFutureExt as _;
448
449 self.get_ipv6_addresses().map_ok(|addresses| {
450 addresses.into_iter().filter(|address| address.octets()[..2] == [0xfe, 0x80]).collect()
451 })
452 }
453
454 pub async fn get_netstack_version(&self) -> Result<InEffectNetstackVersion, Error> {
459 let netstack_migration_state = self.get_netstack_migration_state().await?;
460 Ok(netstack_migration_state.get_netstack_version().await?.into())
461 }
462
463 pub async fn set_user_netstack_version(&self, version: NetstackVersion) -> Result<(), Error> {
468 let netstack_migration_control = self.get_netstack_migration_control().await?;
469 Ok(netstack_migration_control.set_user_netstack_version(Some(&version.into())).await?)
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476 use assert_matches::assert_matches;
477 use futures::StreamExt as _;
478 use test_case::test_case;
479 use {
480 fidl_fuchsia_net as fnet, fidl_fuchsia_net_interfaces as finterfaces,
481 fuchsia_async as fasync,
482 };
483
484 struct MockStateTester {
485 expected_state: Vec<Box<dyn FnOnce(finterfaces::WatcherRequest) + Send + 'static>>,
486 }
487
488 impl MockStateTester {
489 fn new() -> Self {
490 Self { expected_state: vec![] }
491 }
492
493 pub fn create_facade_and_serve_state(
494 self,
495 ) -> (NetstackFacade, impl std::future::Future<Output = ()>) {
496 let (interfaces_state, stream_future) = self.build_state_and_watcher();
497 (
498 NetstackFacade { interfaces_state: interfaces_state.into(), ..Default::default() },
499 stream_future,
500 )
501 }
502
503 fn push_state(
504 mut self,
505 request: impl FnOnce(finterfaces::WatcherRequest) + Send + 'static,
506 ) -> Self {
507 self.expected_state.push(Box::new(request));
508 self
509 }
510
511 fn build_state_and_watcher(
512 self,
513 ) -> (finterfaces::StateProxy, impl std::future::Future<Output = ()>) {
514 let (proxy, mut stream) =
515 fidl::endpoints::create_proxy_and_stream::<finterfaces::StateMarker>();
516 let stream_fut = async move {
517 match stream.next().await {
518 Some(Ok(finterfaces::StateRequest::GetWatcher { watcher, .. })) => {
519 let mut into_stream = watcher.into_stream();
520 for expected in self.expected_state {
521 let () = expected(into_stream.next().await.unwrap().unwrap());
522 }
523 let finterfaces::WatcherRequest::Watch { responder } =
524 into_stream.next().await.unwrap().unwrap();
525 let () = responder
526 .send(&finterfaces::Event::Idle(finterfaces::Empty {}))
527 .unwrap();
528 }
529 err => panic!("Error in request handler: {:?}", err),
530 }
531 };
532 (proxy, stream_fut)
533 }
534
535 fn expect_get_ipv6_addresses(self, result: Vec<fnet::Subnet>) -> Self {
536 let addresses = result
537 .into_iter()
538 .map(|addr| finterfaces::Address { addr: Some(addr), ..Default::default() })
539 .collect();
540 self.push_state(move |req| match req {
541 finterfaces::WatcherRequest::Watch { responder } => responder
542 .send(&finterfaces::Event::Existing(finterfaces::Properties {
543 addresses: Some(addresses),
544 ..Default::default()
545 }))
546 .unwrap(),
547 })
548 }
549 }
550
551 #[fasync::run_singlethreaded(test)]
552 async fn test_get_ipv6_addresses() {
553 let ipv6_octets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
554
555 let ipv6_address = fnet::Subnet {
556 addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr: ipv6_octets }),
557 prefix_len: 137,
559 };
560 let ipv4_address = fnet::Subnet {
561 addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [0, 1, 2, 3] }),
562 prefix_len: 139,
564 };
565 let all_addresses = [ipv6_address.clone(), ipv4_address.clone()];
566 let (facade, stream_fut) = MockStateTester::new()
567 .expect_get_ipv6_addresses(all_addresses.to_vec())
568 .create_facade_and_serve_state();
569 let facade_fut = async move {
570 let result_address: Vec<_> = facade.get_ipv6_addresses().await.unwrap();
571 assert_eq!(result_address, [std::net::Ipv6Addr::from(ipv6_octets)]);
572 };
573 futures::future::join(facade_fut, stream_fut).await;
574 }
575
576 #[fasync::run_singlethreaded(test)]
577 async fn test_get_link_local_ipv6_addresses() {
578 let ipv6_address = fnet::Subnet {
579 addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address {
580 addr: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
581 }),
582 prefix_len: 137,
584 };
585 let link_local_ipv6_octets = [0xfe, 0x80, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
586 let link_local_ipv6_address = fnet::Subnet {
587 addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr: link_local_ipv6_octets }),
588 prefix_len: 139,
590 };
591 let ipv4_address = fnet::Subnet {
592 addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: [0, 1, 2, 3] }),
593 prefix_len: 141,
595 };
596 let all_addresses =
597 [ipv6_address.clone(), link_local_ipv6_address.clone(), ipv4_address.clone()];
598 let (facade, stream_fut) = MockStateTester::new()
599 .expect_get_ipv6_addresses(all_addresses.to_vec())
600 .create_facade_and_serve_state();
601 let facade_fut = async move {
602 let result_address: Vec<_> = facade.get_link_local_ipv6_addresses().await.unwrap();
603 assert_eq!(result_address, [std::net::Ipv6Addr::from(link_local_ipv6_octets)]);
604 };
605 futures::future::join(facade_fut, stream_fut).await;
606 }
607
608 #[test_case(Value::String("ns2".to_string()), Some(NetstackVersion::Netstack2); "ns2")]
609 #[test_case(Value::String("ns3".to_string()), Some(NetstackVersion::Netstack3); "ns3")]
610 #[test_case(Value::String("netstack2".to_string()), Some(NetstackVersion::Netstack2);
611 "netstack2")]
612 #[test_case(Value::String("netstack3".to_string()), Some(NetstackVersion::Netstack3);
613 "netstack3")]
614 #[test_case(Value::String("invalid".to_string()), None; "invalid_string")]
615 #[test_case(Value::Bool(false), None; "invalid_value")]
616 fn test_convert_netstack_version_from_json_value(
617 json_value: Value,
618 expected_version: Option<NetstackVersion>,
619 ) {
620 let version: Result<NetstackVersion, Error> = json_value.try_into();
621 match expected_version {
622 Some(expected_version) => assert_eq!(version.expect("parse version"), expected_version),
623 None => assert_matches!(version, Err(_)),
624 }
625 }
626
627 #[test_case(fnet_stack_migration::NetstackVersion::Netstack2, NetstackVersion::Netstack2;
628 "netstack2")]
629 #[test_case(fnet_stack_migration::NetstackVersion::Netstack3, NetstackVersion::Netstack3;
630 "netstack3")]
631 fn test_convert_netstack_version_from_fidl(
632 fidl_version: fnet_stack_migration::NetstackVersion,
633 expected_version: NetstackVersion,
634 ) {
635 assert_eq!(NetstackVersion::from(fidl_version), expected_version);
636 assert_eq!(
637 NetstackVersion::from(fnet_stack_migration::VersionSetting { version: fidl_version }),
638 expected_version
639 );
640 }
641}