| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #![cfg(test)] |
| |
| use std::mem::size_of; |
| |
| use fidl_fuchsia_net as net; |
| use fidl_fuchsia_netstack as netstack; |
| use fidl_fuchsia_netstack_ext::RouteTable; |
| use fidl_fuchsia_sys as sys; |
| use fuchsia_async::{self as fasync, DurationExt as _, TimeoutExt as _}; |
| use fuchsia_component::client::AppBuilder; |
| use fuchsia_zircon as zx; |
| |
| use anyhow::{self, Context}; |
| use futures::{ |
| future, Future, FutureExt as _, StreamExt as _, TryFutureExt as _, TryStreamExt as _, |
| }; |
| use net_types::ethernet::Mac; |
| use net_types::ip::{self as net_types_ip, Ip}; |
| use net_types::{ |
| LinkLocalAddress as _, MulticastAddress as _, SpecifiedAddress as _, Witness as _, |
| }; |
| use netstack_testing_common::constants::{eth as eth_consts, ipv6 as ipv6_consts}; |
| use netstack_testing_common::environments::{KnownServices, Netstack, Netstack2}; |
| use netstack_testing_common::{ |
| send_ra_with_router_lifetime, setup_network, setup_network_with, sleep, write_ndp_message, |
| EthertapName, Result, ASYNC_EVENT_CHECK_INTERVAL, ASYNC_EVENT_NEGATIVE_CHECK_TIMEOUT, |
| ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT, NDP_MESSAGE_TTL, |
| }; |
| use netstack_testing_macros::variants_test; |
| use packet::ParsablePacket as _; |
| use packet_formats::ethernet::{EtherType, EthernetFrame, EthernetFrameLengthCheck}; |
| use packet_formats::icmp::mld::MldPacket; |
| use packet_formats::icmp::ndp::{ |
| options::{NdpOption, PrefixInformation}, |
| NeighborAdvertisement, NeighborSolicitation, RouterAdvertisement, RouterSolicitation, |
| }; |
| use packet_formats::icmp::{IcmpParseArgs, Icmpv6Packet}; |
| use packet_formats::ip::IpProto; |
| use packet_formats::testutil::{parse_icmp_packet_in_ip_packet_in_ethernet_frame, parse_ip_packet}; |
| |
| /// The expected number of Router Solicitations sent by the netstack when an |
| /// interface is brought up as a host. |
| const EXPECTED_ROUTER_SOLICIATIONS: u8 = 3; |
| |
| /// The expected interval between sending Router Solicitation messages when |
| /// soliciting IPv6 routers. |
| const EXPECTED_ROUTER_SOLICITATION_INTERVAL: zx::Duration = zx::Duration::from_seconds(4); |
| |
| /// The expected number of Neighbor Solicitations sent by the netstack when |
| /// performing Duplicate Address Detection. |
| const EXPECTED_DUP_ADDR_DETECT_TRANSMITS: u8 = 1; |
| |
| /// The expected interval between sending Neighbor Solicitation messages when |
| /// performing Duplicate Address Detection. |
| const EXPECTED_DAD_RETRANSMIT_TIMER: zx::Duration = zx::Duration::from_seconds(1); |
| |
| /// As per [RFC 7217 section 6] Hosts SHOULD introduce a random delay between 0 and |
| /// `IDGEN_DELAY` before trying a new tentative address. |
| /// |
| /// [RFC 7217]: https://tools.ietf.org/html/rfc7217#section-6 |
| const DAD_IDGEN_DELAY: zx::Duration = zx::Duration::from_seconds(1); |
| |
| /// Launches a new netstack with the endpoint and returns the IPv6 addresses |
| /// initially assigned to it. |
| /// |
| /// If `run_netstack_and_get_ipv6_addrs_for_endpoint` returns successfully, it |
| /// is guaranteed that the launched netstack has been terminated. Note, if |
| /// `run_netstack_and_get_ipv6_addrs_for_endpoint` does not return successfully, |
| /// the launched netstack will still be terminated, but no guarantees are made |
| /// about when that will happen. |
| async fn run_netstack_and_get_ipv6_addrs_for_endpoint<N: Netstack>( |
| endpoint: &netemul::TestEndpoint<'_>, |
| launcher: &sys::LauncherProxy, |
| name: String, |
| ) -> Result<Vec<net::Subnet>> { |
| // Launch the netstack service. |
| |
| let mut app = AppBuilder::new(N::VERSION.get_url()) |
| .spawn(launcher) |
| .context("failed to spawn netstack")?; |
| let netstack = app |
| .connect_to_service::<netstack::NetstackMarker>() |
| .context("failed to connect to netstack service")?; |
| |
| // Add the device and get its interface state from netstack. |
| // TODO(fxbug.dev/48907) Support Network Device. This helper fn should use stack.fidl |
| // and be agnostic over interface type. |
| let id = netstack |
| .add_ethernet_device( |
| &name, |
| &mut netstack::InterfaceConfig { |
| name: name[..fidl_fuchsia_posix_socket::INTERFACE_NAME_LENGTH.into()].to_string(), |
| filepath: "/fake/filepath/for_test".to_string(), |
| metric: 0, |
| }, |
| endpoint |
| .get_ethernet() |
| .await |
| .context("add_ethernet_device requires an Ethernet endpoint")?, |
| ) |
| .await |
| .context("add_ethernet_device FIDL error")? |
| .map_err(fuchsia_zircon::Status::from_raw) |
| .context("add_ethernet_device error")?; |
| let interface = netstack |
| .get_interfaces2() |
| .await |
| .context("failed to get interfaces")? |
| .into_iter() |
| .find(|interface| interface.id == id) |
| .ok_or(anyhow::anyhow!("failed to find added ethernet device"))?; |
| |
| // Kill the netstack. |
| // |
| // Note, simply dropping `component_controller` would also kill the netstack |
| // but we explicitly kill it and wait for the terminated event before |
| // proceeding. |
| let () = app.kill().context("failed to kill app")?; |
| let _exit_status = app.wait().await.context("failed to observe netstack termination")?; |
| |
| Ok(interface.ipv6addrs) |
| } |
| |
| /// Test that across netstack runs, a device will initially be assigned the same |
| /// IPv6 addresses. |
| #[variants_test] |
| async fn consistent_initial_ipv6_addrs<E: netemul::Endpoint>(name: &str) -> Result { |
| let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?; |
| let env = sandbox |
| .create_environment(name, &[KnownServices::SecureStash]) |
| .context("failed to create environment")?; |
| let launcher = env.get_launcher().context("failed to get launcher")?; |
| let endpoint = sandbox |
| .create_endpoint::<netemul::Ethernet, _>(name.ethertap_compatible_name()) |
| .await |
| .context("failed to create endpoint")?; |
| |
| // Make sure netstack uses the same addresses across runs for a device. |
| let first_run_addrs = run_netstack_and_get_ipv6_addrs_for_endpoint::<Netstack2>( |
| &endpoint, |
| &launcher, |
| name.to_string(), |
| ) |
| .await?; |
| let second_run_addrs = run_netstack_and_get_ipv6_addrs_for_endpoint::<Netstack2>( |
| &endpoint, |
| &launcher, |
| name.to_string(), |
| ) |
| .await?; |
| assert_eq!(first_run_addrs, second_run_addrs); |
| |
| Ok(()) |
| } |
| |
| /// Tests that `EXPECTED_ROUTER_SOLICIATIONS` Router Solicitation messages are transmitted |
| /// when the interface is brought up. |
| #[variants_test] |
| async fn sends_router_solicitations<E: netemul::Endpoint>(name: &str) -> Result { |
| let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?; |
| let (_network, _environment, _netstack, _iface, fake_ep) = |
| setup_network::<E, _>(&sandbox, name).await?; |
| |
| // Make sure exactly `EXPECTED_ROUTER_SOLICIATIONS` RS messages are transmited |
| // by the netstack. |
| let mut observed_rs = 0; |
| loop { |
| // When we have already observed the expected number of RS messages, do a |
| // negative check to make sure that we don't send anymore. |
| let extra_timeout = if observed_rs == EXPECTED_ROUTER_SOLICIATIONS { |
| ASYNC_EVENT_NEGATIVE_CHECK_TIMEOUT |
| } else { |
| ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT |
| }; |
| |
| let ret = fake_ep |
| .frame_stream() |
| .try_filter_map(|(data, dropped)| { |
| assert_eq!(dropped, 0); |
| let mut observed_slls = Vec::new(); |
| future::ok( |
| parse_icmp_packet_in_ip_packet_in_ethernet_frame::< |
| net_types_ip::Ipv6, |
| _, |
| RouterSolicitation, |
| _, |
| >(&data, |p| { |
| for option in p.body().iter() { |
| if let NdpOption::SourceLinkLayerAddress(a) = option { |
| let mut mac_bytes = [0; 6]; |
| mac_bytes.copy_from_slice(&a[..size_of::<Mac>()]); |
| observed_slls.push(Mac::new(mac_bytes)); |
| } else { |
| // We should only ever have an NDP Source Link-Layer Address |
| // option in a RS. |
| panic!("unexpected option in RS = {:?}", option); |
| } |
| } |
| }) |
| .map_or( |
| None, |
| |(_src_mac, dst_mac, src_ip, dst_ip, ttl, _message, _code)| { |
| Some((dst_mac, src_ip, dst_ip, ttl, observed_slls)) |
| }, |
| ), |
| ) |
| }) |
| .try_next() |
| .map(|r| r.context("error getting OnData event")) |
| .on_timeout((EXPECTED_ROUTER_SOLICITATION_INTERVAL + extra_timeout).after_now(), || { |
| // If we already observed `EXPECTED_ROUTER_SOLICIATIONS` RS, then we shouldn't |
| // have gotten any more; the timeout is expected. |
| if observed_rs == EXPECTED_ROUTER_SOLICIATIONS { |
| return Ok(None); |
| } |
| |
| return Err(anyhow::anyhow!("timed out waiting for the {}-th RS", observed_rs)); |
| }) |
| .await?; |
| |
| let (dst_mac, src_ip, dst_ip, ttl, observed_slls) = match ret { |
| Some((dst_mac, src_ip, dst_ip, ttl, observed_slls)) => { |
| (dst_mac, src_ip, dst_ip, ttl, observed_slls) |
| } |
| None => break, |
| }; |
| |
| assert_eq!( |
| dst_mac, |
| Mac::from(&net_types_ip::Ipv6::ALL_ROUTERS_LINK_LOCAL_MULTICAST_ADDRESS) |
| ); |
| |
| // DAD should have resolved for the link local IPv6 address that is assigned to |
| // the interface when it is first brought up. When a link local address is |
| // assigned to the interface, it should be used for transmitted RS messages. |
| if observed_rs > 0 { |
| assert!(src_ip.is_specified()) |
| } |
| |
| assert_eq!(dst_ip, net_types_ip::Ipv6::ALL_ROUTERS_LINK_LOCAL_MULTICAST_ADDRESS.get()); |
| |
| assert_eq!(ttl, NDP_MESSAGE_TTL); |
| |
| // The Router Solicitation should only ever have at max 1 source |
| // link-layer option. |
| assert!(observed_slls.len() <= 1); |
| let observed_sll = observed_slls.into_iter().nth(0); |
| if src_ip.is_specified() { |
| if observed_sll.is_none() { |
| panic!("expected source-link-layer address option if RS has a specified source IP address"); |
| } |
| } else if observed_sll.is_some() { |
| panic!("unexpected source-link-layer address option for RS with unspecified source IP address"); |
| } |
| |
| observed_rs += 1; |
| } |
| |
| assert_eq!(observed_rs, EXPECTED_ROUTER_SOLICIATIONS); |
| |
| Ok(()) |
| } |
| |
| /// Tests that both stable and temporary SLAAC addresses are generated for a SLAAC prefix. |
| #[variants_test] |
| async fn slaac_with_privacy_extensions<E: netemul::Endpoint>(name: &str) -> Result { |
| let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?; |
| let (_network, environment, _netstack, iface, fake_ep) = |
| setup_network::<E, _>(&sandbox, name).await?; |
| |
| // Wait for a Router Solicitation. |
| // |
| // The first RS should be sent immediately. |
| let () = fake_ep |
| .frame_stream() |
| .try_filter_map(|(data, dropped)| { |
| assert_eq!(dropped, 0); |
| future::ok( |
| parse_icmp_packet_in_ip_packet_in_ethernet_frame::< |
| net_types_ip::Ipv6, |
| _, |
| RouterSolicitation, |
| _, |
| >(&data, |_| {}) |
| .map_or(None, |_| Some(())), |
| ) |
| }) |
| .try_next() |
| .map(|r| r.context("error getting OnData event")) |
| .on_timeout(ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.after_now(), || { |
| Err(anyhow::anyhow!("timed out waiting for RS packet")) |
| }) |
| .await? |
| .ok_or(anyhow::anyhow!("failed to get next OnData event"))?; |
| |
| // Send a Router Advertisement with information for a SLAAC prefix. |
| let ra = RouterAdvertisement::new( |
| 0, /* current_hop_limit */ |
| false, /* managed_flag */ |
| false, /* other_config_flag */ |
| 0, /* router_lifetime */ |
| 0, /* reachable_time */ |
| 0, /* retransmit_timer */ |
| ); |
| let pi = PrefixInformation::new( |
| ipv6_consts::PREFIX.prefix(), /* prefix_length */ |
| false, /* on_link_flag */ |
| true, /* autonomous_address_configuration_flag */ |
| 99999, /* valid_lifetime */ |
| 99999, /* preferred_lifetime */ |
| ipv6_consts::PREFIX.network(), /* prefix */ |
| ); |
| let options = [NdpOption::PrefixInformation(&pi)]; |
| let () = write_ndp_message::<&[u8], _>( |
| eth_consts::MAC_ADDR, |
| Mac::from(&net_types_ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS), |
| ipv6_consts::LINK_LOCAL_ADDR, |
| net_types_ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(), |
| ra, |
| &options, |
| &fake_ep, |
| ) |
| .await |
| .context("failed to write NDP message")?; |
| |
| // Wait for the SLAAC addresses to be generated. |
| // |
| // We expect two addresses for the SLAAC prefixes to be assigned to the NIC as the |
| // netstack should generate both a stable and temporary SLAAC address. |
| let interface_state = environment |
| .connect_to_service::<fidl_fuchsia_net_interfaces::StateMarker>() |
| .context("failed to connect to fuchsia.net.interfaces/State")?; |
| let expected_addrs = 2; |
| fidl_fuchsia_net_interfaces_ext::wait_interface_with_id( |
| fidl_fuchsia_net_interfaces_ext::event_stream_from_state(&interface_state)?, |
| &mut fidl_fuchsia_net_interfaces_ext::InterfaceState::Unknown(iface.id()), |
| |fidl_fuchsia_net_interfaces_ext::Properties { addresses, .. }| { |
| if addresses |
| .iter() |
| .filter_map( |
| |&fidl_fuchsia_net_interfaces_ext::Address { |
| addr: fidl_fuchsia_net::Subnet { addr, prefix_len: _ }, |
| }| { |
| match addr { |
| net::IpAddress::Ipv4(net::Ipv4Address { addr: _ }) => None, |
| net::IpAddress::Ipv6(net::Ipv6Address { addr }) => { |
| // TODO(https://github.com/rust-lang/rust/issues/80967): use bool::then_some. |
| ipv6_consts::PREFIX |
| .contains(&net_types_ip::Ipv6Addr::new(addr)) |
| .then(|| ()) |
| } |
| } |
| }, |
| ) |
| .count() |
| == expected_addrs as usize |
| { |
| Some(()) |
| } else { |
| None |
| } |
| }, |
| ) |
| .map_err(anyhow::Error::from) |
| .on_timeout( |
| (EXPECTED_DAD_RETRANSMIT_TIMER * EXPECTED_DUP_ADDR_DETECT_TRANSMITS * expected_addrs |
| + ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT) |
| .after_now(), |
| || Err(anyhow::anyhow!("timed out")), |
| ) |
| .await |
| .context("failed to wait for SLAAC addresses to be generated") |
| } |
| |
| /// Adds `ipv6_consts::LINK_LOCAL_ADDR` to the interface and makes sure a Neighbor Solicitation |
| /// message is transmitted by the netstack for DAD. |
| /// |
| /// Calls `fail_dad_fn` after the DAD message is observed so callers can simulate a remote |
| /// node that has some interest in the same address. |
| async fn add_address_for_dad< |
| 'a, |
| 'b: 'a, |
| R: 'b + Future<Output = Result>, |
| FN: FnOnce(&'b netemul::TestInterface<'a>, &'b netemul::TestFakeEndpoint<'a>) -> R, |
| >( |
| iface: &'b netemul::TestInterface<'a>, |
| fake_ep: &'b netemul::TestFakeEndpoint<'a>, |
| fail_dad_fn: FN, |
| ) -> Result { |
| let () = iface |
| .add_ip_addr(net::Subnet { |
| addr: net::IpAddress::Ipv6(net::Ipv6Address { |
| addr: ipv6_consts::LINK_LOCAL_ADDR.ipv6_bytes(), |
| }), |
| prefix_len: 64, |
| }) |
| .await?; |
| |
| // The first DAD message should be sent immediately. |
| let ret = fake_ep |
| .frame_stream() |
| .try_filter_map(|(data, dropped)| { |
| assert_eq!(dropped, 0); |
| future::ok( |
| parse_icmp_packet_in_ip_packet_in_ethernet_frame::< |
| net_types_ip::Ipv6, |
| _, |
| NeighborSolicitation, |
| _, |
| >(&data, |p| assert_eq!(p.body().iter().count(), 0)) |
| .map_or(None, |(_src_mac, dst_mac, src_ip, dst_ip, ttl, message, _code)| { |
| // If the NS is not for the address we just added, this is for some |
| // other address. We ignore it as it is not relevant to our test. |
| if message.target_address() != &ipv6_consts::LINK_LOCAL_ADDR { |
| return None; |
| } |
| |
| Some((dst_mac, src_ip, dst_ip, ttl)) |
| }), |
| ) |
| }) |
| .try_next() |
| .map(|r| r.context("error getting OnData event")) |
| .on_timeout(ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.after_now(), || { |
| Err(anyhow::anyhow!( |
| "timed out waiting for a neighbor solicitation targetting {}", |
| ipv6_consts::LINK_LOCAL_ADDR |
| )) |
| }) |
| .await? |
| .ok_or(anyhow::anyhow!("failed to get next OnData event"))?; |
| |
| let (dst_mac, src_ip, dst_ip, ttl) = ret; |
| let expected_dst = ipv6_consts::LINK_LOCAL_ADDR.to_solicited_node_address(); |
| assert_eq!(src_ip, net_types_ip::Ipv6::UNSPECIFIED_ADDRESS); |
| assert_eq!(dst_ip, expected_dst.get()); |
| assert_eq!(dst_mac, Mac::from(&expected_dst)); |
| assert_eq!(ttl, NDP_MESSAGE_TTL); |
| |
| let () = fail_dad_fn(iface, fake_ep).await?; |
| |
| Ok(()) |
| } |
| |
| /// Tests that if the netstack attempts to assign an address to an interface, and a remote node |
| /// is already assigned the address or attempts to assign the address at the same time, DAD |
| /// fails on the local interface. |
| /// |
| /// If no remote node has any interest in an address the netstack is attempting to assign to |
| /// an interface, DAD should succeed. |
| #[variants_test] |
| async fn duplicate_address_detection<E: netemul::Endpoint>(name: &str) -> Result { |
| /// Makes sure that `ipv6_consts::LINK_LOCAL_ADDR` is not assigned to the interface after the |
| /// DAD resolution time. |
| async fn check_address_failed_dad(iface: &netemul::TestInterface<'_>) -> Result { |
| // Clocks sometimes jump in infrastructure, which can cause a timer to expire prematurely. |
| // Fortunately such jumps are rarely seen in quick succession - if we repeatedly wait for |
| // shorter durations we can be reasonably sure that the intended amount of time truly did |
| // elapse. It is expected that at most one timer worth of time may be lost. |
| const STEP: zx::Duration = zx::Duration::from_millis(10); |
| let duration = EXPECTED_DAD_RETRANSMIT_TIMER * EXPECTED_DUP_ADDR_DETECT_TRANSMITS |
| + ASYNC_EVENT_NEGATIVE_CHECK_TIMEOUT; |
| let iterations = |
| (duration + STEP - zx::Duration::from_nanos(1)).into_micros() / STEP.into_micros(); |
| for _ in 0..iterations { |
| let () = fasync::Timer::new(fasync::Time::after(STEP)).await; |
| } |
| |
| let addr = net::Subnet { |
| addr: net::IpAddress::Ipv6(net::Ipv6Address { |
| addr: ipv6_consts::LINK_LOCAL_ADDR.ipv6_bytes(), |
| }), |
| prefix_len: 64, |
| }; |
| assert!(!iface.get_addrs().await?.iter().any(|a| a == &addr)); |
| |
| Ok(()) |
| } |
| |
| /// Transmits a Neighbor Solicitation message and expects `ipv6_consts::LINK_LOCAL_ADDR` |
| /// to not be assigned to the interface after the normal resolution time for DAD. |
| async fn fail_dad_with_ns( |
| iface: &netemul::TestInterface<'_>, |
| fake_ep: &netemul::TestFakeEndpoint<'_>, |
| ) -> Result { |
| let snmc = ipv6_consts::LINK_LOCAL_ADDR.to_solicited_node_address(); |
| let () = write_ndp_message::<&[u8], _>( |
| eth_consts::MAC_ADDR, |
| Mac::from(&snmc), |
| net_types_ip::Ipv6::UNSPECIFIED_ADDRESS, |
| snmc.get(), |
| NeighborSolicitation::new(ipv6_consts::LINK_LOCAL_ADDR), |
| &[], |
| fake_ep, |
| ) |
| .await |
| .context("failed to write NDP message")?; |
| |
| check_address_failed_dad(iface).await |
| } |
| |
| /// Transmits a Neighbor Advertisement message and expects `ipv6_consts::LINK_LOCAL_ADDR` |
| /// to not be assigned to the interface after the normal resolution time for DAD. |
| async fn fail_dad_with_na( |
| iface: &netemul::TestInterface<'_>, |
| fake_ep: &netemul::TestFakeEndpoint<'_>, |
| ) -> Result { |
| let () = write_ndp_message::<&[u8], _>( |
| eth_consts::MAC_ADDR, |
| Mac::from(&net_types_ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS), |
| ipv6_consts::LINK_LOCAL_ADDR, |
| net_types_ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(), |
| NeighborAdvertisement::new( |
| false, /* router_flag */ |
| false, /* solicited_flag */ |
| false, /* override_flag */ |
| ipv6_consts::LINK_LOCAL_ADDR, |
| ), |
| &[NdpOption::TargetLinkLayerAddress(ð_consts::MAC_ADDR.bytes())], |
| fake_ep, |
| ) |
| .await |
| .context("failed to write NDP message")?; |
| |
| check_address_failed_dad(iface).await |
| } |
| |
| let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?; |
| let (_network, environment, _netstack, iface, fake_ep) = |
| setup_network::<E, _>(&sandbox, name).await?; |
| |
| // Add an address and expect it to fail DAD because we simulate another node |
| // performing DAD at the same time. |
| let () = add_address_for_dad(&iface, &fake_ep, fail_dad_with_ns).await?; |
| |
| // Add an address and expect it to fail DAD because we simulate another node |
| // already owning the address. |
| let () = add_address_for_dad(&iface, &fake_ep, fail_dad_with_na).await?; |
| |
| // Add the address, and make sure it gets assigned. |
| let () = add_address_for_dad(&iface, &fake_ep, |_, _| async { Ok(()) }).await?; |
| |
| let interface_state = environment |
| .connect_to_service::<fidl_fuchsia_net_interfaces::StateMarker>() |
| .context("failed to connect to fuchsia.net.interfaces/State")?; |
| fidl_fuchsia_net_interfaces_ext::wait_interface_with_id( |
| fidl_fuchsia_net_interfaces_ext::event_stream_from_state(&interface_state)?, |
| &mut fidl_fuchsia_net_interfaces_ext::InterfaceState::Unknown(iface.id()), |
| |fidl_fuchsia_net_interfaces_ext::Properties { addresses, .. }| { |
| addresses.iter().find_map( |
| |&fidl_fuchsia_net_interfaces_ext::Address { |
| addr: fidl_fuchsia_net::Subnet { addr, prefix_len: _ }, |
| }| { |
| match addr { |
| net::IpAddress::Ipv6(net::Ipv6Address { addr }) => { |
| if ipv6_consts::LINK_LOCAL_ADDR == net_types_ip::Ipv6Addr::new(addr) { |
| Some(()) |
| } else { |
| None |
| } |
| } |
| net::IpAddress::Ipv4(_) => None, |
| } |
| }, |
| ) |
| }, |
| ) |
| .map_err(anyhow::Error::from) |
| .on_timeout( |
| (EXPECTED_DAD_RETRANSMIT_TIMER * EXPECTED_DUP_ADDR_DETECT_TRANSMITS |
| + ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT) |
| .after_now(), |
| || Err(anyhow::anyhow!("timed out")), |
| ) |
| .await |
| .with_context(|| { |
| format!("failed to wait for address {} to be assigned", ipv6_consts::LINK_LOCAL_ADDR) |
| }) |
| } |
| |
| #[variants_test] |
| async fn router_and_prefix_discovery<E: netemul::Endpoint>(name: &str) -> Result { |
| async fn check_route_table<P>(netstack: &netstack::NetstackProxy, pred: P) -> Result<()> |
| where |
| P: Fn(&Vec<netstack::RouteTableEntry>) -> bool, |
| { |
| let check_attempts = ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.into_seconds() |
| / ASYNC_EVENT_CHECK_INTERVAL.into_seconds(); |
| for attempt in 0..check_attempts { |
| let () = sleep(ASYNC_EVENT_CHECK_INTERVAL.into_seconds()).await; |
| let route_table = |
| netstack.get_route_table().await.context("failed to get route table")?; |
| if pred(&route_table) { |
| return Ok(()); |
| } else { |
| let route_table = RouteTable::new(route_table) |
| .display() |
| .context("failed to format route table")?; |
| println!("route table at attempt={}:\n{}", attempt, route_table); |
| } |
| } |
| Err(anyhow::anyhow!( |
| "timed out on waiting for a route table entry after {} seconds", |
| ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.into_seconds(), |
| )) |
| } |
| |
| let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?; |
| let (_network, _environment, netstack, iface, fake_ep) = |
| setup_network::<E, _>(&sandbox, name).await.context("failed to setup network")?; |
| |
| let pi = PrefixInformation::new( |
| ipv6_consts::PREFIX.prefix(), /* prefix_length */ |
| true, /* on_link_flag */ |
| false, /* autonomous_address_configuration_flag */ |
| 1000, /* valid_lifetime */ |
| 0, /* preferred_lifetime */ |
| ipv6_consts::PREFIX.network(), /* prefix */ |
| ); |
| let options = [NdpOption::PrefixInformation(&pi)]; |
| let () = send_ra_with_router_lifetime(&fake_ep, 1000, &options) |
| .await |
| .context("failed to send router advertisement")?; |
| |
| // Test that the default router should be discovered after it is advertised. |
| let () = check_route_table(&netstack, |route_table| { |
| route_table.iter().any(|netstack::RouteTableEntry { destination, gateway, .. }| { |
| if let net::IpAddress::Ipv6(gateway) = gateway { |
| if let net::IpAddress::Ipv6(destination) = destination { |
| let gateway = net_types_ip::Ipv6Addr::new(gateway.addr); |
| let destination = net_types_ip::Ipv6Addr::new(destination.addr); |
| if destination == net_types_ip::Ipv6::UNSPECIFIED_ADDRESS |
| && gateway == ipv6_consts::LINK_LOCAL_ADDR |
| { |
| return true; |
| } |
| } |
| } |
| false |
| }) |
| }) |
| .await |
| .context("failed when checking route table for default route")?; |
| |
| // Test that the prefix should be discovered after it is advertised. |
| let () = check_route_table(&netstack, |route_table| { |
| route_table.iter().any(|netstack::RouteTableEntry { destination, nicid, .. }| { |
| if let net::IpAddress::Ipv6(dest) = destination { |
| let destination = net_types_ip::Ipv6Addr::new(dest.addr); |
| if destination == ipv6_consts::PREFIX.network() && u64::from(*nicid) == iface.id() { |
| return true; |
| } |
| } |
| false |
| }) |
| }) |
| .await |
| .context("failed when checking route table for the on-link route")?; |
| |
| Ok(()) |
| } |
| |
| #[variants_test] |
| async fn slaac_regeneration_after_dad_failure<E: netemul::Endpoint>(name: &str) -> Result { |
| // Expects an NS message for DAD within timeout and returns the target address of the message. |
| async fn expect_ns_message_in( |
| fake_ep: &netemul::TestFakeEndpoint<'_>, |
| timeout: zx::Duration, |
| ) -> Result<net_types_ip::Ipv6Addr> { |
| fake_ep |
| .frame_stream() |
| .try_filter_map(|(data, dropped)| { |
| assert_eq!(dropped, 0); |
| future::ok( |
| parse_icmp_packet_in_ip_packet_in_ethernet_frame::< |
| net_types_ip::Ipv6, |
| _, |
| NeighborSolicitation, |
| _, |
| >(&data, |p| assert_eq!(p.body().iter().count(), 0)) |
| .map_or(None, |(_src_mac, _dst_mac, _src_ip, _dst_ip, _ttl, message, _code)| { |
| // If the NS target_address does not have the prefix we have advertised, |
| // this is for some other address. We ignore it as it is not relevant to |
| // our test. |
| if !ipv6_consts::PREFIX.contains(message.target_address()) { |
| return None; |
| } |
| |
| Some(*message.target_address()) |
| }), |
| ) |
| }) |
| .try_next() |
| .map(|r| r.context("error getting OnData event")) |
| .on_timeout(timeout.after_now(), || { |
| Err(anyhow::anyhow!( |
| "timed out waiting for a neighbor solicitation targetting address of prefix: {}", |
| ipv6_consts::PREFIX, |
| )) |
| }) |
| .await? |
| .ok_or(anyhow::anyhow!("failed to get next OnData event")) |
| } |
| |
| let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?; |
| let (_network, environment, _netstack, iface, fake_ep) = |
| setup_network_with::<E, _, _>(&sandbox, name, &[KnownServices::SecureStash]).await?; |
| |
| // Send a Router Advertisement with information for a SLAAC prefix. |
| let ra = RouterAdvertisement::new( |
| 0, /* current_hop_limit */ |
| false, /* managed_flag */ |
| false, /* other_config_flag */ |
| 0, /* router_lifetime */ |
| 0, /* reachable_time */ |
| 0, /* retransmit_timer */ |
| ); |
| let pi = PrefixInformation::new( |
| ipv6_consts::PREFIX.prefix(), /* prefix_length */ |
| false, /* on_link_flag */ |
| true, /* autonomous_address_configuration_flag */ |
| 99999, /* valid_lifetime */ |
| 99999, /* preferred_lifetime */ |
| ipv6_consts::PREFIX.network(), /* prefix */ |
| ); |
| let options = [NdpOption::PrefixInformation(&pi)]; |
| let () = write_ndp_message::<&[u8], _>( |
| eth_consts::MAC_ADDR, |
| Mac::from(&net_types_ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS), |
| ipv6_consts::LINK_LOCAL_ADDR, |
| net_types_ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(), |
| ra, |
| &options, |
| &fake_ep, |
| ) |
| .await |
| .context("failed to write RA message")?; |
| |
| let tried_address = expect_ns_message_in(&fake_ep, ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT) |
| .await |
| .context("failed to get a neighbour solicitation")?; |
| |
| // We pretend there is a duplicate address situation. |
| let snmc = tried_address.to_solicited_node_address(); |
| let () = write_ndp_message::<&[u8], _>( |
| eth_consts::MAC_ADDR, |
| Mac::from(&snmc), |
| net_types_ip::Ipv6::UNSPECIFIED_ADDRESS, |
| snmc.get(), |
| NeighborSolicitation::new(tried_address), |
| &[], |
| &fake_ep, |
| ) |
| .await |
| .context("failed to write DAD message")?; |
| |
| let target_address = |
| expect_ns_message_in(&fake_ep, DAD_IDGEN_DELAY + ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT) |
| .await |
| .context("failed to get a neighbour solicitation")?; |
| |
| // We expect two addresses for the SLAAC prefixes to be assigned to the NIC as the |
| // netstack should generate both a stable and temporary SLAAC address. |
| let expected_addrs = 2; |
| let interface_state = environment |
| .connect_to_service::<fidl_fuchsia_net_interfaces::StateMarker>() |
| .context("failed to connect to fuchsia.net.interfaces/State")?; |
| let () = fidl_fuchsia_net_interfaces_ext::wait_interface_with_id( |
| fidl_fuchsia_net_interfaces_ext::event_stream_from_state(&interface_state)?, |
| &mut fidl_fuchsia_net_interfaces_ext::InterfaceState::Unknown(iface.id()), |
| |fidl_fuchsia_net_interfaces_ext::Properties { addresses, .. }| { |
| // We have to make sure 2 things: |
| // 1. We have `expected_addrs` addrs which have the advertised prefix for the |
| // interface. |
| // 2. The last tried address should be among the addresses for the interface. |
| let (slaac_addrs, has_target_addr) = addresses.iter().fold( |
| (0, false), |
| |(mut slaac_addrs, mut has_target_addr), &fidl_fuchsia_net_interfaces_ext::Address { addr: fidl_fuchsia_net::Subnet { addr, prefix_len: _ } }| { |
| match addr { |
| net::IpAddress::Ipv6(net::Ipv6Address { addr }) => { |
| let configured_addr = net_types_ip::Ipv6Addr::new(addr); |
| assert!(configured_addr != tried_address, |
| "unexpected address ({}) assigned to the interface which previously failed DAD", |
| configured_addr |
| ); |
| if ipv6_consts::PREFIX.contains(&configured_addr) { |
| slaac_addrs += 1; |
| } |
| if configured_addr == target_address { |
| has_target_addr = true; |
| } |
| } |
| net::IpAddress::Ipv4(_) => {} |
| } |
| (slaac_addrs, has_target_addr) |
| }, |
| ); |
| |
| assert!( |
| slaac_addrs <= expected_addrs, |
| "more addresses found than expected, found {}, expected {}", |
| slaac_addrs, |
| expected_addrs |
| ); |
| if slaac_addrs == expected_addrs && has_target_addr { |
| Some(()) |
| } else { |
| None |
| } |
| }, |
| ) |
| .map_err(anyhow::Error::from) |
| .on_timeout( |
| (EXPECTED_DAD_RETRANSMIT_TIMER * EXPECTED_DUP_ADDR_DETECT_TRANSMITS * expected_addrs |
| + ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT) |
| .after_now(), |
| || Err(anyhow::anyhow!("timed out")), |
| ) |
| .await |
| .context("failed to wait for SLAAC addresses")?; |
| Ok(()) |
| } |
| |
| #[variants_test] |
| async fn sends_mld_reports<E: netemul::Endpoint>(name: &str) -> Result { |
| let sandbox = netemul::TestSandbox::new().context("error creating sandbox")?; |
| let (_network, _environment, _netstack, iface, fake_ep) = |
| setup_network::<E, _>(&sandbox, name).await.context("error setting up networking")?; |
| |
| // Add an address so we join the address's solicited node multicast group. |
| let () = iface |
| .add_ip_addr(net::Subnet { |
| addr: net::IpAddress::Ipv6(net::Ipv6Address { |
| addr: ipv6_consts::LINK_LOCAL_ADDR.ipv6_bytes(), |
| }), |
| prefix_len: 64, |
| }) |
| .await |
| .context("error adding IP address")?; |
| let snmc = ipv6_consts::LINK_LOCAL_ADDR.to_solicited_node_address(); |
| |
| let stream = fake_ep |
| .frame_stream() |
| .map(|r| r.context("error getting OnData event")) |
| .try_filter_map(|(data, dropped)| { |
| async move { |
| assert_eq!(dropped, 0); |
| let mut data = &data[..]; |
| |
| let eth = EthernetFrame::parse(&mut data, EthernetFrameLengthCheck::Check) |
| .context("error parsing ethernet frame")?; |
| |
| if eth.ethertype() != Some(EtherType::Ipv6) { |
| // Ignore non-IPv6 packets. |
| return Ok(None); |
| } |
| |
| let (mut payload, src_ip, dst_ip, proto, ttl) = |
| parse_ip_packet::<net_types_ip::Ipv6>(&data) |
| .context("error parsing IPv6 packet")?; |
| |
| if proto != IpProto::Icmpv6 { |
| // Ignore non-ICMPv6 packets. |
| return Ok(None); |
| } |
| |
| let icmp = Icmpv6Packet::parse(&mut payload, IcmpParseArgs::new(src_ip, dst_ip)) |
| .context("error parsing ICMPv6 packet")?; |
| |
| let mld = if let Icmpv6Packet::Mld(mld) = icmp { |
| mld |
| } else { |
| // Ignore non-MLD packets. |
| return Ok(None); |
| }; |
| |
| // As per RFC 3590 section 4, |
| // |
| // MLD Report and Done messages are sent with a link-local address as |
| // the IPv6 source address, if a valid address is available on the |
| // interface. If a valid link-local address is not available (e.g., one |
| // has not been configured), the message is sent with the unspecified |
| // address (::) as the IPv6 source address. |
| assert!(!src_ip.is_specified() || src_ip.is_linklocal(), "MLD messages must be sent from the unspecified or link local address; src_ip = {}", src_ip); |
| |
| assert!(dst_ip.is_multicast(), "all MLD messages must be sent to a multicast address; dst_ip = {}", dst_ip); |
| |
| // As per RFC 2710 section 3, |
| // |
| // All MLD messages described in this document are sent with a |
| // link-local IPv6 Source Address, an IPv6 Hop Limit of 1, ... |
| assert_eq!(ttl, 1, "MLD messages must have a hop limit of 1"); |
| |
| let report = if let MldPacket::MulticastListenerReport(report) = mld { |
| report |
| } else { |
| // Ignore non-report messages. |
| return Ok(None); |
| }; |
| |
| let group_addr = report.body().group_addr; |
| assert!(group_addr.is_multicast(), "MLD reports must only be sent for multicast addresses; group_addr = {}", group_addr); |
| |
| if group_addr != *snmc { |
| // We are only interested in the report for the solicited node |
| // multicast group we joined. |
| return Ok(None); |
| } |
| |
| assert_eq!(dst_ip, group_addr, "the destination of an MLD report should be the multicast group the report is for"); |
| |
| Ok(Some(())) |
| } |
| }); |
| futures::pin_mut!(stream); |
| let () = stream |
| .try_next() |
| .on_timeout(ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.after_now(), || { |
| return Err(anyhow::anyhow!("timed out waiting for the MLD report")); |
| }) |
| .await? |
| .context("error getting our expected MLD report")?; |
| |
| Ok(()) |
| } |