blob: 8321920ff5c6eda390d4184ddd5b6f22ad687746 [file] [log] [blame]
// 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 fidl_fuchsia_net as fnet;
use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
use fidl_fuchsia_net_routes as fnet_routes;
use fidl_fuchsia_net_routes_ext as fnet_routes_ext;
use fuchsia_async::{DurationExt as _, TimeoutExt as _};
use futures::{FutureExt as _, StreamExt as _};
use net_declare::{net_ip_v4, std_ip_v4};
use net_types::{
ip::{self as net_types_ip, Ipv4, Ipv4Addr},
MulticastAddress as _,
};
use netemul::RealmUdpSocket;
use netstack_testing_common::{
interfaces::{self, TestInterfaceExt},
realms::{Netstack, NetstackVersion, TestSandboxExt},
setup_network, ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT,
};
use netstack_testing_macros::netstack_test;
use packet::ParsablePacket as _;
use packet_formats::{
ethernet::{EtherType, EthernetFrame, EthernetFrameLengthCheck},
igmp::{
messages::{
IgmpGroupRecordType, IgmpMembershipReportV1, IgmpMembershipReportV2,
IgmpMembershipReportV3,
},
IgmpMessage, MessageType,
},
ip::Ipv4Proto,
testutil::parse_ip_packet,
};
use std::pin::pin;
use test_case::test_case;
fn check_igmpv1v2_report<'a, M: MessageType<&'a [u8], FixedHeader = net_types_ip::Ipv4Addr>>(
dst_ip: net_types_ip::Ipv4Addr,
igmp: IgmpMessage<&'a [u8], M>,
expected_group: net_types_ip::Ipv4Addr,
) -> bool {
let group_addr = igmp.group_addr();
assert!(
group_addr.is_multicast(),
"IGMP reports must only be sent for multicast addresses; group_addr = {}",
group_addr
);
if group_addr != expected_group {
// We are only interested in the report for the multicast group we
// joined.
return false;
}
assert_eq!(
dst_ip, group_addr,
"the destination of an IGMP report should be the multicast group the report is for"
);
true
}
fn check_igmpv1_report(
dst_ip: net_types_ip::Ipv4Addr,
mut payload: &[u8],
expected_group: net_types_ip::Ipv4Addr,
) -> bool {
check_igmpv1v2_report(
dst_ip,
IgmpMessage::<_, IgmpMembershipReportV1>::parse(&mut payload, ())
.expect("error parsing IGMP message"),
expected_group,
)
}
fn check_igmpv2_report(
dst_ip: net_types_ip::Ipv4Addr,
mut payload: &[u8],
expected_group: net_types_ip::Ipv4Addr,
) -> bool {
check_igmpv1v2_report(
dst_ip,
IgmpMessage::<_, IgmpMembershipReportV2>::parse(&mut payload, ())
.expect("error parsing IGMP message"),
expected_group,
)
}
fn check_igmpv3_report(
dst_ip: net_types_ip::Ipv4Addr,
mut payload: &[u8],
expected_group: net_types_ip::Ipv4Addr,
) -> bool {
let igmp = IgmpMessage::<_, IgmpMembershipReportV3>::parse(&mut payload, ())
.expect("error parsing IGMP message");
let records = igmp
.body()
.iter()
.map(|record| {
let hdr = record.header();
(*hdr.multicast_addr(), hdr.record_type(), record.sources().to_vec())
})
.collect::<Vec<_>>();
assert_eq!(
records,
[(expected_group, Ok(IgmpGroupRecordType::ChangeToExcludeMode), Vec::new(),)]
);
assert_eq!(
dst_ip,
net_ip_v4!("224.0.0.22"),
"IGMPv3 reports should should be sent to the IGMPv3 routers address",
);
true
}
fn check_igmp_report(
igmp_version: Option<fnet_interfaces_admin::IgmpVersion>,
netstack_version: NetstackVersion,
dst_ip: net_types_ip::Ipv4Addr,
payload: &[u8],
expected_group: net_types_ip::Ipv4Addr,
) -> bool {
match igmp_version {
Some(version) => match version {
fnet_interfaces_admin::IgmpVersion::V1 => {
check_igmpv1_report(dst_ip, payload, expected_group)
}
fnet_interfaces_admin::IgmpVersion::V2 => {
check_igmpv2_report(dst_ip, payload, expected_group)
}
fnet_interfaces_admin::IgmpVersion::V3 => {
check_igmpv3_report(dst_ip, payload, expected_group)
}
_ => panic!("unknown IGMP version {:?}", version),
},
None => match netstack_version {
NetstackVersion::Netstack2 { tracing: false, fast_udp: false } => {
check_igmpv3_report(dst_ip, payload, expected_group)
}
NetstackVersion::Netstack3 => check_igmpv2_report(dst_ip, payload, expected_group),
v @ (NetstackVersion::Netstack2 { tracing: _, fast_udp: _ }
| NetstackVersion::ProdNetstack2
| NetstackVersion::ProdNetstack3) => {
panic!("netstack_test should only be parameterized with Netstack2 or Netstack3: got {:?}", v);
}
},
}
}
#[netstack_test]
#[test_case(Some(fnet_interfaces_admin::IgmpVersion::V1); "igmpv1")]
#[test_case(Some(fnet_interfaces_admin::IgmpVersion::V2); "igmpv2")]
#[test_case(Some(fnet_interfaces_admin::IgmpVersion::V3); "igmpv3")]
#[test_case(None; "default")]
async fn sends_igmp_reports<N: Netstack>(
name: &str,
igmp_version: Option<fnet_interfaces_admin::IgmpVersion>,
) {
const INTERFACE_ADDR: std::net::Ipv4Addr = std_ip_v4!("192.168.0.1");
const MULTICAST_ADDR: std::net::Ipv4Addr = std_ip_v4!("224.1.2.3");
let sandbox = netemul::TestSandbox::new().expect("error creating sandbox");
let (_network, realm, iface, fake_ep) =
setup_network::<N>(&sandbox, name, None).await.expect("error setting up network");
if let Some(igmp_version) = igmp_version {
let gen_config = |igmp_version| fnet_interfaces_admin::Configuration {
ipv4: Some(fnet_interfaces_admin::Ipv4Configuration {
igmp: Some(fnet_interfaces_admin::IgmpConfiguration {
version: Some(igmp_version),
..Default::default()
}),
..Default::default()
}),
..Default::default()
};
let control = iface.control();
let new_config = gen_config(igmp_version);
let old_config = gen_config(fnet_interfaces_admin::IgmpVersion::V3);
assert_eq!(
control
.set_configuration(&new_config)
.await
.expect("set_configuration fidl error")
.expect("failed to set interface configuration"),
old_config,
);
assert_eq!(
control
.set_configuration(&new_config)
.await
.expect("set_configuration fidl error")
.expect("failed to set interface configuration"),
new_config,
);
assert_matches::assert_matches!(
control
.get_configuration()
.await
.expect("get_configuration fidl error")
.expect("failed to get interface configuration"),
fnet_interfaces_admin::Configuration {
ipv4: Some(fnet_interfaces_admin::Ipv4Configuration {
igmp: Some(fnet_interfaces_admin::IgmpConfiguration {
version: Some(got),
..
}),
..
}),
..
} => assert_eq!(got, igmp_version)
);
}
let addr = fnet::Ipv4Address { addr: INTERFACE_ADDR.octets() };
let _address_state_provider = interfaces::add_subnet_address_and_route_wait_assigned(
&iface,
fnet::Subnet { addr: fnet::IpAddress::Ipv4(addr), prefix_len: 24 },
fidl_fuchsia_net_interfaces_admin::AddressParameters::default(),
)
.await
.expect("add subnet address and route");
let sock = fuchsia_async::net::UdpSocket::bind_in_realm(
&realm,
std::net::SocketAddrV4::new(std::net::Ipv4Addr::UNSPECIFIED, 0).into(),
)
.await
.expect("error creating socket");
let () = sock
.as_ref()
.join_multicast_v4(&MULTICAST_ADDR, &INTERFACE_ADDR)
.expect("error joining multicast group");
let net_types_ip_multicast_addr = net_types_ip::Ipv4Addr::new(MULTICAST_ADDR.octets());
let stream = fake_ep.frame_stream().map(|r| r.expect("error getting OnData event")).filter_map(
|(data, dropped)| {
async move {
assert_eq!(dropped, 0);
let mut data = &data[..];
// Do not check the frame length as the size of IGMP reports may be less
// than the minimum ethernet frame length and our virtual (netemul) interface
// does not pad runt ethernet frames before transmission.
let eth = EthernetFrame::parse(&mut data, EthernetFrameLengthCheck::NoCheck)
.expect("error parsing ethernet frame");
if eth.ethertype() != Some(EtherType::Ipv4) {
// Ignore non-IPv4 packets.
return None;
}
let (payload, src_ip, dst_ip, proto, ttl) =
parse_ip_packet::<net_types_ip::Ipv4>(&data)
.expect("error parsing IPv4 packet");
if proto != Ipv4Proto::Igmp {
// Ignore non-IGMP packets.
return None;
}
// TODO(https://fxbug.dev/42180878): Don't send IGMP reports before a local address
// is assigned.
if N::VERSION != NetstackVersion::Netstack3 {
assert_eq!(
src_ip,
net_types_ip::Ipv4Addr::new(INTERFACE_ADDR.octets()),
"IGMP messages must be sent from an address assigned to the NIC",
);
}
// As per RFC 2236 section 2,
//
// All IGMP messages described in this document are sent with
// IP TTL 1, ...
assert_eq!(ttl, 1, "IGMP messages must have a TTL of 1");
check_igmp_report(
igmp_version,
N::VERSION,
dst_ip,
payload,
net_types_ip_multicast_addr,
)
.then_some(())
}
},
);
let mut stream = pin!(stream);
let () = stream
.next()
.on_timeout(ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.after_now(), || {
panic!("timed out waiting for the IGMP report");
})
.await
.expect("error getting our expected IGMP report");
}
#[netstack_test]
async fn all_ones_broadcast<N: Netstack>(name: &str) {
let sandbox = netemul::TestSandbox::new().expect("error creating sandbox");
let name_suffixes = ["a", "b", "c"];
let realms = name_suffixes.map(|suffix| {
sandbox
.create_netstack_realm::<N, _>(format!("{name}_{suffix}"))
.unwrap_or_else(|e| panic!("create realm {suffix}: {e:?}"))
});
let network = sandbox.create_network(name).await.expect("create network");
let addr_subnets = [
net_declare::fidl_subnet!("192.168.0.1/24"),
net_declare::fidl_subnet!("192.168.0.2/24"),
net_declare::fidl_subnet!("192.168.0.3/24"),
];
// Keep same as first `addr_subnet`.
const SENDER_IP: std::net::IpAddr = net_declare::std_ip!("192.168.0.1");
const DEFAULT_SUBNET: net_types::ip::Subnet<Ipv4Addr> =
net_declare::net_subnet_v4!("0.0.0.0/0");
let mut ifaces = Vec::new();
for (i, realm) in realms.iter().enumerate() {
let iface = realm
.join_network(&network, format!("{name}_{}", name_suffixes[i]))
.await
.expect("join network");
iface.add_address(addr_subnets[i]).await.expect("add address");
iface.apply_nud_flake_workaround().await.expect("apply nud flake workaround");
let global_route_set = iface
.create_authenticated_global_route_set::<Ipv4>()
.await
.expect("create authenticated route set");
// Add an on-link default route so that the netstacks should each be
// able to receive broadcasts from each other.
let default_route = fnet_routes_ext::Route::<Ipv4> {
action: fnet_routes_ext::RouteAction::Forward(fnet_routes_ext::RouteTarget {
outbound_interface: iface.id(),
next_hop: None,
}),
destination: DEFAULT_SUBNET,
properties: fnet_routes_ext::RouteProperties {
specified_properties: fnet_routes_ext::SpecifiedRouteProperties {
metric: fnet_routes::SpecifiedMetric::InheritedFromInterface(
fnet_routes::Empty,
),
},
},
};
assert!(
global_route_set
.add_route(&default_route.try_into().expect("convert to FIDL route"))
.await
.expect("adding default route should not get FIDL error")
.expect("adding default route should succeed"),
"should have newly-added default route"
);
ifaces.push(iface);
}
let sending_realm = &realms[0];
// Using 1024 as arbitrary port for sending/receiving.
const PORT: u16 = 1024;
let make_socket = |realm| async move {
let socket = fuchsia_async::net::UdpSocket::bind_in_realm(
realm,
std::net::SocketAddr::new(std::net::Ipv4Addr::UNSPECIFIED.into(), PORT),
)
.await
.expect("bind in realm");
socket.set_broadcast(true).expect("set broadcast");
socket
};
let sending_socket = make_socket(sending_realm).await;
let mut receiving_sockets = Vec::new();
for receiving_realm in &realms[1..] {
receiving_sockets.push(make_socket(receiving_realm).await);
}
const PAYLOAD: &str = "hello";
assert_eq!(
sending_socket
.send_to(
PAYLOAD.as_bytes(),
std::net::SocketAddr::new(std::net::Ipv4Addr::BROADCAST.into(), PORT),
)
.await
.expect("send should succeed"),
PAYLOAD.len()
);
let mut buf = [0u8; 16];
for receiving_socket in receiving_sockets {
let (n, received_from) = receiving_socket
.recv_from(&mut buf)
.map(Some)
.on_timeout(ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT, || None)
.await
.expect("should not have timed out")
.expect("recv_from");
assert_eq!(n, PAYLOAD.len());
assert_eq!(&buf[..n], PAYLOAD.as_bytes());
assert_eq!(received_from, std::net::SocketAddr::new(SENDER_IP, PORT));
}
}