blob: 29b755519e00da8c9074a8c9994186f3a3d9650d [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 std::num::NonZeroU16;
use std::str::FromStr as _;
use fidl_fuchsia_net_dhcp as net_dhcp;
use fidl_fuchsia_net_dhcpv6 as net_dhcpv6;
use fidl_fuchsia_net_interfaces as net_interfaces;
use fidl_fuchsia_net_name as net_name;
use fuchsia_async::{DurationExt as _, TimeoutExt as _};
use fuchsia_zircon as zx;
use anyhow::Context as _;
use futures::future::{self, FusedFuture, Future, FutureExt as _};
use futures::stream::{self, StreamExt as _, TryStreamExt as _};
use net_declare::{fidl_ip_v4, fidl_ip_v6, fidl_subnet, std_ip_v6};
use net_types::ethernet::Mac;
use net_types::ip as net_types_ip;
use net_types::Witness;
use netstack_testing_common::constants::{eth as eth_consts, ipv6 as ipv6_consts};
use netstack_testing_common::environments::{
KnownServices, Manager, NetCfg, Netstack2, TestSandboxExt as _,
};
use netstack_testing_common::{write_ndp_message, Result, ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT};
use netstack_testing_macros::variants_test;
use packet::serialize::{InnerPacketBuilder as _, Serializer as _};
use packet::ParsablePacket as _;
use packet_formats::ethernet::{EtherType, EthernetFrameBuilder};
use packet_formats::icmp::ndp::{
options::{NdpOption, RecursiveDnsServer},
RouterAdvertisement,
};
use packet_formats::ip::IpProto;
use packet_formats::ipv6::Ipv6PacketBuilder;
use packet_formats::testutil::parse_ip_packet_in_ethernet_frame;
use packet_formats::udp::{UdpPacket, UdpPacketBuilder, UdpParseArgs};
use packet_formats_dhcp::v6;
/// Keep polling the lookup admin's DNS servers until it returns `expect`.
async fn poll_lookup_admin<
F: Unpin + FusedFuture + Future<Output = Result<fuchsia_component::client::ExitStatus>>,
>(
lookup_admin: &net_name::LookupAdminProxy,
expect: Vec<fidl_fuchsia_net::SocketAddress>,
mut wait_for_netmgr_fut: &mut F,
poll_wait: zx::Duration,
retry_count: u64,
) -> Result {
for i in 0..retry_count {
let () = futures::select! {
() = fuchsia_async::Timer::new(poll_wait.after_now()).fuse() => {
Ok(())
}
wait_for_netmgr_res = wait_for_netmgr_fut => {
Err(anyhow::anyhow!("the network manager unexpectedly exited with exit status = {:?}", wait_for_netmgr_res?))
}
}?;
let servers = lookup_admin.get_dns_servers().await.context("Failed to get DNS servers")?;
println!("attempt {}) Got DNS servers {:?}", i, servers);
if servers == expect {
return Ok(());
}
}
// Too many retries.
Err(anyhow::anyhow!(
"timed out waiting for DNS server configurations; retry_count={}, poll_wait={:?}",
retry_count,
poll_wait,
))
}
/// Tests that Netstack exposes DNS servers discovered dynamically and NetworkManager
/// configures the Lookup service.
#[variants_test]
async fn test_discovered_dns<E: netemul::Endpoint, M: Manager>(name: &str) -> Result {
const SERVER_ADDR: fidl_fuchsia_net::Subnet = fidl_subnet!("192.168.0.1/24");
/// DNS server served by DHCP.
const DHCP_DNS_SERVER: fidl_fuchsia_net::Ipv4Address = fidl_ip_v4!("123.12.34.56");
/// DNS server served by NDP.
const NDP_DNS_SERVER: fidl_fuchsia_net::Ipv6Address = fidl_ip_v6!("20a::1234:5678");
/// Maximum number of times we'll poll `LookupAdmin` to check DNS configuration
/// succeeded.
const RETRY_COUNT: u64 = 60;
/// Duration to sleep between polls.
const POLL_WAIT: zx::Duration = zx::Duration::from_seconds(1);
const DEFAULT_DNS_PORT: u16 = 53;
let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?;
let network = sandbox.create_network("net").await.context("failed to create network")?;
let server_environment = sandbox
.create_netstack_environment_with::<Netstack2, _, _>(
format!("{}_server", name),
vec![
KnownServices::DhcpServer.into_launch_service(),
KnownServices::SecureStash.into_launch_service(),
],
)
.context("failed to create server environment")?;
let client_environment = sandbox
.create_netstack_environment_with::<Netstack2, _, _>(
format!("{}_client", name),
&[KnownServices::LookupAdmin],
)
.context("failed to create client environment")?;
let _server_iface = server_environment
.join_network::<E, _>(
&network,
"server-ep",
&netemul::InterfaceConfig::StaticIp(SERVER_ADDR),
)
.await
.context("failed to configure server networking")?;
let dhcp_server = server_environment
.connect_to_service::<net_dhcp::Server_Marker>()
.context("failed to connext to DHCP server")?;
let dhcp_server_ref = &dhcp_server;
// TODO(fxbug.dev/62554): derive these from SERVER_ADDR.
let () = stream::iter(
[
fidl_fuchsia_net_dhcp::Parameter::IpAddrs(vec![fidl_ip_v4!("192.168.0.1")]),
fidl_fuchsia_net_dhcp::Parameter::AddressPool(fidl_fuchsia_net_dhcp::AddressPool {
prefix_length: Some(25),
range_start: Some(fidl_ip_v4!("192.168.0.2")),
range_stop: Some(fidl_ip_v4!("192.168.0.5")),
..fidl_fuchsia_net_dhcp::AddressPool::EMPTY
}),
fidl_fuchsia_net_dhcp::Parameter::BoundDeviceNames(vec!["eth2".to_string()]),
]
.iter_mut(),
)
.map(Ok)
.try_for_each_concurrent(None, |parameter| async move {
dhcp_server_ref
.set_parameter(parameter)
.await
.context("failed to call dhcp/Server.SetParameter")?
.map_err(fuchsia_zircon::Status::from_raw)
.with_context(|| format!("dhcp/Server.SetParameter({:?}) returned error", parameter))
})
.await?;
let () = dhcp_server
.set_option(&mut net_dhcp::Option_::DomainNameServer(vec![DHCP_DNS_SERVER]))
.await
.context("Failed to set DNS option")?
.map_err(zx::Status::from_raw)
.context("dhcp/Server.SetOption returned error")?;
let () = dhcp_server
.start_serving()
.await
.context("failed to call dhcp/Server.StartServing")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("dhcp/Server.StartServing returned error")?;
// Start networking on client environment.
let _client_iface = client_environment
.join_network::<E, _>(&network, "client-ep", &netemul::InterfaceConfig::Dhcp)
.await
.context("failed to configure client networking")?;
// Send a Router Advertisement with DNS server configurations.
let fake_ep = network.create_fake_endpoint()?;
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 addresses = [NDP_DNS_SERVER.addr.into()];
let rdnss = RecursiveDnsServer::new(9999, &addresses);
let options = [NdpOption::RecursiveDnsServer(rdnss)];
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")?;
// Start the network manager on the client.
//
// The network manager should listen for DNS server events from the netstack and
// configure the DNS resolver accordingly.
let launcher =
client_environment.get_launcher().context("failed to create launcher for client env")?;
let mut netmgr =
fuchsia_component::client::launch(&launcher, M::PKG_URL.to_string(), M::testing_args())
.context("launch the network manager")?;
// The list of servers we expect to retrieve from `fuchsia.net.name/LookupAdmin`.
let expect = vec![
fidl_fuchsia_net::SocketAddress::Ipv6(fidl_fuchsia_net::Ipv6SocketAddress {
address: NDP_DNS_SERVER,
port: DEFAULT_DNS_PORT,
zone_index: 0,
}),
fidl_fuchsia_net::SocketAddress::Ipv4(fidl_fuchsia_net::Ipv4SocketAddress {
address: DHCP_DNS_SERVER,
port: DEFAULT_DNS_PORT,
}),
];
// Poll LookupAdmin until we get the servers we want or after too many tries.
let lookup_admin = client_environment
.connect_to_service::<net_name::LookupAdminMarker>()
.context("failed to connect to LookupAdmin")?;
let mut wait_for_netmgr_fut = netmgr.wait().fuse();
poll_lookup_admin(&lookup_admin, expect, &mut wait_for_netmgr_fut, POLL_WAIT, RETRY_COUNT)
.await
.context("poll lookup admin")
}
/// Tests that DHCPv6 exposes DNS servers discovered dynamically and the network manager
/// configures the Lookup service.
#[variants_test]
async fn test_discovered_dhcpv6_dns<E: netemul::Endpoint>(name: &str) -> Result {
/// DHCPv6 server IP.
const DHCPV6_SERVER: net_types_ip::Ipv6Addr =
net_types_ip::Ipv6Addr::new(std_ip_v6!("fe80::1").octets());
/// DNS server served by DHCPv6.
const DHCPV6_DNS_SERVER: fidl_fuchsia_net::Ipv6Address = fidl_ip_v6!("20a::1234:5678");
/// Maximum number of times we'll poll `LookupAdmin` to check DNS configuration
/// succeeded.
const RETRY_COUNT: u64 = 60;
/// Duration to sleep between polls.
const POLL_WAIT: zx::Duration = zx::Duration::from_seconds(1);
const DEFAULT_DNS_PORT: u16 = 53;
let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?;
let network = sandbox.create_network("net").await.context("failed to create network")?;
let environment = sandbox
.create_netstack_environment_with::<Netstack2, _, _>(
format!("{}_client", name),
&[KnownServices::Dhcpv6Client, KnownServices::LookupAdmin],
)
.context("failed to create environment")?;
// Start the network manager on the client.
//
// The network manager should listen for DNS server events from the DHCPv6 client and
// configure the DNS resolver accordingly.
let launcher = environment.get_launcher().context("failed to create launcher for env")?;
let mut netmgr = fuchsia_component::client::launch(
&launcher,
NetCfg::PKG_URL.to_string(),
NetCfg::testing_args(),
)
.context("launch the network manager")?;
// Start networking on client environment.
let endpoint = network.create_endpoint::<E, _>(name).await.context("create endpoint")?;
let () = endpoint.set_link_up(true).await.context("set link up")?;
let endpoint_mount_path = E::dev_path("ep");
let endpoint_mount_path = endpoint_mount_path.as_path();
let () = environment
.add_virtual_device(&endpoint, endpoint_mount_path)
.with_context(|| format!("add virtual device {}", endpoint_mount_path.display()))?;
// Make sure the Netstack got the new device added.
let interface_state = environment
.connect_to_service::<net_interfaces::StateMarker>()
.context("connect to fuchsia.net.interfaces/State service")?;
let mut wait_for_netmgr_fut = netmgr.wait().fuse();
let _: (u64, String) = netstack_testing_common::wait_for_non_loopback_interface_up(
&interface_state,
&mut wait_for_netmgr_fut,
None,
ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT,
)
.await
.context("wait for a non loopback interface to come up")?;
// Wait for the DHCPv6 information request.
let fake_ep = network.create_fake_endpoint()?;
let (src_mac, dst_mac, src_ip, dst_ip, src_port, tx_id) = fake_ep
.frame_stream()
.try_filter_map(|(data, dropped)| {
assert_eq!(dropped, 0);
future::ok(parse_ip_packet_in_ethernet_frame::<net_types_ip::Ipv6>(&data).map_or(
None,
|(mut body, src_mac, dst_mac, src_ip, dst_ip, _proto, _ttl)| {
// DHCPv6 messages are held in UDP packets.
let udp = match UdpPacket::parse(&mut body, UdpParseArgs::new(src_ip, dst_ip)) {
Ok(o) => o,
Err(_) => return None,
};
// We only care about UDP packets directed at a DHCPv6 server.
if udp.dst_port().get() != net_dhcpv6::RELAY_AGENT_AND_SERVER_PORT {
return None;
}
// We only care about DHCPv6 messages.
let mut body = udp.body();
let msg = match v6::Message::parse(&mut body, ()) {
Ok(o) => o,
Err(_) => return None,
};
// We only care about DHCPv6 information requests.
if msg.msg_type() != v6::MessageType::InformationRequest {
return None;
}
// We only care about DHCPv6 information requests for DNS servers.
for opt in msg.options() {
if let v6::DhcpOption::Oro(codes) = opt {
if !codes.contains(&v6::OptionCode::DnsServers) {
return None;
}
}
}
Some((
src_mac,
dst_mac,
src_ip,
dst_ip,
udp.src_port(),
msg.transaction_id().clone(),
))
},
))
})
.try_next()
.map(|r| r.context("error getting OnData event"))
.on_timeout(ASYNC_EVENT_POSITIVE_CHECK_TIMEOUT.after_now(), || {
return Err(anyhow::anyhow!("timed out waiting for the DHCPv6 Information request"));
})
.await
.context("wait for DHCPv6 Information Request")?
.ok_or(anyhow::anyhow!("ran out of incoming frames"))?;
assert!(src_ip.is_unicast_linklocal(), "src ip {} should be a unicast link-local", src_ip);
assert_eq!(
Ok(std::net::Ipv6Addr::from(dst_ip.ipv6_bytes())),
std::net::Ipv6Addr::from_str(
net_dhcpv6::RELAY_AGENT_AND_SERVER_LINK_LOCAL_MULTICAST_ADDRESS
),
"dst ip should be the DHCPv6 servers multicast IP"
);
assert_eq!(
src_port.map(|x| x.get()),
Some(net_dhcpv6::DEFAULT_CLIENT_PORT),
"should use RFC defined src port"
);
// Send the DHCPv6 reply.
let options = [
v6::DhcpOption::ServerId(&[]),
v6::DhcpOption::DnsServers(vec![std::net::Ipv6Addr::from(DHCPV6_DNS_SERVER.addr)]),
];
let builder = v6::MessageBuilder::new(v6::MessageType::Reply, tx_id, &options);
let ser = builder
.into_serializer()
.encapsulate(UdpPacketBuilder::new(
DHCPV6_SERVER,
src_ip,
NonZeroU16::new(net_dhcpv6::RELAY_AGENT_AND_SERVER_PORT),
NonZeroU16::new(net_dhcpv6::DEFAULT_CLIENT_PORT)
.expect("default DHCPv6 client port is non-zero"),
))
.encapsulate(Ipv6PacketBuilder::new(
DHCPV6_SERVER,
src_ip,
ipv6_consts::DEFAULT_HOP_LIMIT,
IpProto::Udp,
))
.encapsulate(EthernetFrameBuilder::new(dst_mac, src_mac, EtherType::Ipv6))
.serialize_vec_outer()
.map_err(|_| anyhow::anyhow!("failed to serialize DHCPv6 packet"))?
.unwrap_b();
let () = fake_ep.write(ser.as_ref()).await.context("failed to write to fake endpoint")?;
// The list of servers we expect to retrieve from `fuchsia.net.name/LookupAdmin`.
let expect = vec![fidl_fuchsia_net::SocketAddress::Ipv6(fidl_fuchsia_net::Ipv6SocketAddress {
address: DHCPV6_DNS_SERVER,
port: DEFAULT_DNS_PORT,
zone_index: 0,
})];
// Poll LookupAdmin until we get the servers we want or after too many tries.
let lookup_admin = environment
.connect_to_service::<net_name::LookupAdminMarker>()
.context("failed to connect to LookupAdmin")?;
poll_lookup_admin(&lookup_admin, expect, &mut wait_for_netmgr_fut, POLL_WAIT, RETRY_COUNT)
.await
.context("poll lookup admin")
}