netstack_testing_common/
ndp.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Helpers for tests involving the Neighbor Discovery Protocol.
6
7use crate::constants;
8use anyhow::Context as _;
9use assert_matches::assert_matches;
10use fuchsia_async::{DurationExt as _, TimeoutExt as _};
11use futures::{future, FutureExt as _, Stream, StreamExt as _, TryStreamExt as _};
12use net_types::ip::Ip as _;
13use net_types::Witness as _;
14use packet::serialize::{InnerPacketBuilder, Serializer};
15use packet::Buf;
16use packet_formats::ethernet::{
17    EtherType, EthernetFrameBuilder, EthernetFrameLengthCheck, ETHERNET_MIN_BODY_LEN_NO_TAG,
18};
19use packet_formats::icmp::ndp::options::{NdpOption, NdpOptionBuilder};
20use packet_formats::icmp::ndp::{
21    NeighborAdvertisement, NeighborSolicitation, OptionSequenceBuilder, RouterAdvertisement,
22    RouterSolicitation,
23};
24use packet_formats::icmp::{IcmpMessage, IcmpPacketBuilder, IcmpZeroCode};
25use packet_formats::ip::Ipv6Proto;
26use packet_formats::ipv6::Ipv6PacketBuilder;
27use packet_formats::testutil::parse_icmp_packet_in_ip_packet_in_ethernet_frame;
28use std::fmt::Debug;
29
30/// As per [RFC 4861] sections 4.1-4.5, NDP packets MUST have a hop limit of 255.
31///
32/// [RFC 4861]: https://tools.ietf.org/html/rfc4861
33pub const MESSAGE_TTL: u8 = 255;
34
35/// Create an NDP message with the provided parameters, including IPv6 and
36/// Ethernet headers.
37pub fn create_message<M: IcmpMessage<net_types::ip::Ipv6, Code = IcmpZeroCode> + Debug>(
38    src_mac: net_types::ethernet::Mac,
39    dst_mac: net_types::ethernet::Mac,
40    src_ip: net_types::ip::Ipv6Addr,
41    dst_ip: net_types::ip::Ipv6Addr,
42    message: M,
43    options: &[NdpOptionBuilder<'_>],
44) -> crate::Result<Buf<Vec<u8>>> {
45    Ok(OptionSequenceBuilder::new(options.iter())
46        .into_serializer()
47        .encapsulate(IcmpPacketBuilder::<_, _>::new(src_ip, dst_ip, IcmpZeroCode, message))
48        .encapsulate(Ipv6PacketBuilder::new(src_ip, dst_ip, MESSAGE_TTL, Ipv6Proto::Icmpv6))
49        .encapsulate(EthernetFrameBuilder::new(
50            src_mac,
51            dst_mac,
52            EtherType::Ipv6,
53            ETHERNET_MIN_BODY_LEN_NO_TAG,
54        ))
55        .serialize_vec_outer()
56        .map_err(|e| anyhow::anyhow!("failed to serialize NDP packet: {:?}", e))?
57        .unwrap_b())
58}
59
60/// Write an NDP message to the provided fake endpoint.
61///
62/// Given the source and destination MAC and IP addresses, NDP message and
63/// options, the full NDP packet (including IPv6 and Ethernet headers) will be
64/// transmitted to the fake endpoint's network.
65pub async fn write_message<M: IcmpMessage<net_types::ip::Ipv6, Code = IcmpZeroCode> + Debug>(
66    src_mac: net_types::ethernet::Mac,
67    dst_mac: net_types::ethernet::Mac,
68    src_ip: net_types::ip::Ipv6Addr,
69    dst_ip: net_types::ip::Ipv6Addr,
70    message: M,
71    options: &[NdpOptionBuilder<'_>],
72    ep: &netemul::TestFakeEndpoint<'_>,
73) -> crate::Result {
74    let ser = create_message(src_mac, dst_mac, src_ip, dst_ip, message, options)?;
75    ep.write(ser.as_ref()).await.context("failed to write to fake endpoint")
76}
77
78/// Send Router Advertisement NDP message.
79pub async fn send_ra<'a>(
80    fake_ep: &netemul::TestFakeEndpoint<'a>,
81    ra: RouterAdvertisement,
82    options: &[NdpOptionBuilder<'_>],
83    src_ip: net_types::ip::Ipv6Addr,
84) -> crate::Result {
85    write_message(
86        constants::eth::MAC_ADDR,
87        net_types::ethernet::Mac::from(
88            &net_types::ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS,
89        ),
90        src_ip,
91        net_types::ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
92        ra,
93        options,
94        fake_ep,
95    )
96    .await
97}
98
99/// Send Router Advertisement NDP message with router lifetime.
100pub async fn send_ra_with_router_lifetime<'a>(
101    fake_ep: &netemul::TestFakeEndpoint<'a>,
102    lifetime: u16,
103    options: &[NdpOptionBuilder<'_>],
104    src_ip: net_types::ip::Ipv6Addr,
105) -> crate::Result {
106    let ra = RouterAdvertisement::new(
107        0,        /* current_hop_limit */
108        false,    /* managed_flag */
109        false,    /* other_config_flag */
110        lifetime, /* router_lifetime */
111        0,        /* reachable_time */
112        0,        /* retransmit_timer */
113    );
114    send_ra(fake_ep, ra, options, src_ip).await
115}
116
117/// A result type that can be used to evaluate the outcome of Duplicate Address
118/// Detection (DAD).
119pub type DadState = Result<
120    fidl_fuchsia_net_interfaces::AddressAssignmentState,
121    fidl_fuchsia_net_interfaces_ext::admin::AddressStateProviderError,
122>;
123
124/// Wait for and verify a NS message transmitted by netstack for DAD. Returns
125/// the bytes of the NS message.
126pub async fn expect_dad_neighbor_solicitation(fake_ep: &netemul::TestFakeEndpoint<'_>) -> Vec<u8> {
127    let ret = fake_ep
128        .frame_stream()
129        .try_filter_map(|(data, dropped)| {
130            assert_eq!(dropped, 0);
131            future::ok(
132                parse_icmp_packet_in_ip_packet_in_ethernet_frame::<
133                    net_types::ip::Ipv6,
134                    _,
135                    NeighborSolicitation,
136                    _,
137                >(&data, EthernetFrameLengthCheck::NoCheck, |p| {
138                    assert_matches!(
139                        &p.body().iter().collect::<Vec<_>>()[..],
140                        [NdpOption::Nonce(_)]
141                    );
142                })
143                .map_or(
144                    None,
145                    |(_src_mac, dst_mac, src_ip, dst_ip, ttl, message, _code)| {
146                        // If the NS is not for the address we just added, this is for some
147                        // other address. We ignore it as it is not relevant to our test.
148                        if message.target_address() != &constants::ipv6::LINK_LOCAL_ADDR {
149                            return None;
150                        }
151
152                        Some((dst_mac, src_ip, dst_ip, ttl, data))
153                    },
154                ),
155            )
156        })
157        .try_next()
158        .map(|r| r.context("error getting OnData event"))
159        .on_timeout(crate::ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.after_now(), || {
160            Err(anyhow::anyhow!(
161                "timed out waiting for a neighbor solicitation targetting {}",
162                constants::ipv6::LINK_LOCAL_ADDR
163            ))
164        })
165        .await
166        .unwrap()
167        .expect("failed to get next OnData event");
168
169    let (dst_mac, src_ip, dst_ip, ttl, data) = ret;
170    let expected_dst = constants::ipv6::LINK_LOCAL_ADDR.to_solicited_node_address();
171    assert_eq!(src_ip, net_types::ip::Ipv6::UNSPECIFIED_ADDRESS);
172    assert_eq!(dst_ip, expected_dst.get());
173    assert_eq!(dst_mac, net_types::ethernet::Mac::from(&expected_dst));
174    assert_eq!(ttl, MESSAGE_TTL);
175
176    data
177}
178
179/// Transmit a Neighbor Solicitation message simulating that a node is
180/// performing DAD for `constants::ipv6::LINK_LOCAL_ADDR`.
181pub async fn fail_dad_with_ns(fake_ep: &netemul::TestFakeEndpoint<'_>) {
182    let snmc = constants::ipv6::LINK_LOCAL_ADDR.to_solicited_node_address();
183    write_message(
184        constants::eth::MAC_ADDR,
185        net_types::ethernet::Mac::from(&snmc),
186        net_types::ip::Ipv6::UNSPECIFIED_ADDRESS,
187        snmc.get(),
188        NeighborSolicitation::new(constants::ipv6::LINK_LOCAL_ADDR),
189        &[],
190        fake_ep,
191    )
192    .await
193    .expect("failed to write NDP message");
194}
195
196/// Transmit a Neighbor Advertisement message simulating that a node owns
197/// `constants::ipv6::LINK_LOCAL_ADDR`.
198pub async fn fail_dad_with_na(fake_ep: &netemul::TestFakeEndpoint<'_>) {
199    write_message(
200        constants::eth::MAC_ADDR,
201        net_types::ethernet::Mac::from(
202            &net_types::ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS,
203        ),
204        constants::ipv6::LINK_LOCAL_ADDR,
205        net_types::ip::Ipv6::ALL_NODES_LINK_LOCAL_MULTICAST_ADDRESS.get(),
206        NeighborAdvertisement::new(
207            false, /* router_flag */
208            false, /* solicited_flag */
209            false, /* override_flag */
210            constants::ipv6::LINK_LOCAL_ADDR,
211        ),
212        &[NdpOptionBuilder::TargetLinkLayerAddress(&constants::eth::MAC_ADDR.bytes())],
213        fake_ep,
214    )
215    .await
216    .expect("failed to write NDP message");
217}
218
219async fn dad_state(
220    state_stream: &mut (impl Stream<Item = DadState> + std::marker::Unpin),
221) -> DadState {
222    // The address state provider doesn't buffer events, so we might see the tentative state,
223    // but we might not.
224    let state = match state_stream.by_ref().next().await.expect("state stream not ended") {
225        Ok(fidl_fuchsia_net_interfaces::AddressAssignmentState::Tentative) => {
226            state_stream.by_ref().next().await.expect("state stream not ended")
227        }
228        state => state,
229    };
230    // Ensure errors are terminal.
231    match state {
232        Ok(_) => {}
233        Err(_) => {
234            assert_matches::assert_matches!(state_stream.by_ref().next().await, None)
235        }
236    }
237    state
238}
239
240/// Assert that the address state provider event stream yields an address
241/// removal error, indicating that DAD failed.
242pub async fn assert_dad_failed(
243    mut state_stream: (impl Stream<Item = DadState> + std::marker::Unpin),
244) {
245    assert_matches::assert_matches!(
246        dad_state(&mut state_stream).await,
247        Err(fidl_fuchsia_net_interfaces_ext::admin::AddressStateProviderError::AddressRemoved(
248            fidl_fuchsia_net_interfaces_admin::AddressRemovalReason::DadFailed
249        ))
250    );
251}
252
253/// Assert that the address state provider event stream yields an address
254/// assignment event, implying that DAD succeeded.
255pub async fn assert_dad_success(
256    state_stream: &mut (impl Stream<Item = DadState> + std::marker::Unpin),
257) {
258    assert_matches::assert_matches!(
259        dad_state(state_stream).await,
260        Ok(fidl_fuchsia_net_interfaces::AddressAssignmentState::Assigned)
261    );
262}
263
264/// Wait for a router solicitation message.
265pub async fn wait_for_router_solicitation(fake_ep: &netemul::TestFakeEndpoint<'_>) {
266    let () = fake_ep
267        .frame_stream()
268        .try_filter_map(|(data, dropped)| {
269            assert_eq!(dropped, 0);
270            future::ok(
271                parse_icmp_packet_in_ip_packet_in_ethernet_frame::<
272                    net_types::ip::Ipv6,
273                    _,
274                    RouterSolicitation,
275                    _,
276                >(&data, EthernetFrameLengthCheck::NoCheck, |_| {})
277                .map_or(None, |_| Some(())),
278            )
279        })
280        .try_next()
281        .map(|r| r.context("error getting OnData event"))
282        .on_timeout(crate::ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.after_now(), || {
283            Err(anyhow::anyhow!("timed out waiting for RS packet"))
284        })
285        .await
286        .unwrap()
287        .expect("failed to get next OnData event");
288}