blob: c6b5744b6dde5e10f439baca48dd84df30383b68 [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.
use net_declare::{fidl_ip, fidl_mac, fidl_subnet};
struct IpLayerConfig {
alice_subnet: fidl_fuchsia_net::Subnet,
bob_ip: fidl_fuchsia_net::IpAddress,
}
struct EthernetLayerConfig {
alice_mac: fidl_fuchsia_net::MacAddress,
bob_mac: fidl_fuchsia_net::MacAddress,
ip_layer: IpLayerConfig,
}
/// The network configuration used for TAP-like example.
///
/// Note that the tests in this crate run in parallel against the same Netstack,
/// so the IP addresses between tests must be different to avoid interference.
const CONFIG_FOR_TAP_LIKE: EthernetLayerConfig = EthernetLayerConfig {
alice_mac: fidl_mac!("02:03:04:05:06:07"),
bob_mac: fidl_mac!("02:AA:BB:CC:DD:EE"),
ip_layer: IpLayerConfig {
alice_subnet: fidl_subnet!("192.168.0.1/24"),
bob_ip: fidl_ip!("192.168.0.2"),
},
};
/// The network configuration used for TUN-like example.
///
/// Note that the tests in this crate run in parallel against the same Netstack,
/// so the IP addresses between tests must be different to avoid interference.
const CONFIG_FOR_TUN_LIKE: IpLayerConfig = IpLayerConfig {
alice_subnet: fidl_subnet!("192.168.86.1/24"),
bob_ip: fidl_ip!("192.168.86.2"),
};
/// The port identifier used in examples.
const PORT_ID: u8 = 0;
/// Creates a new [`fidl_fuchsia_net_tun::DeviceConfig`] and
/// [`fidl_fuchsia_net_tun::DevicePortConfig`] using the provided `frame_type` and `mac`.
fn new_device_and_port_config(
frame_types: Vec<fidl_fuchsia_hardware_network::FrameType>,
mac: Option<fidl_fuchsia_net::MacAddress>,
) -> (fidl_fuchsia_net_tun::DeviceConfig, fidl_fuchsia_net_tun::DevicePortConfig) {
let rx_types = frame_types;
let tx_types = rx_types
.iter()
.map(|frame_type| fidl_fuchsia_hardware_network::FrameTypeSupport {
type_: *frame_type,
features: 0,
supported_flags: fidl_fuchsia_hardware_network::TxFlags::empty(),
})
.collect();
let device_config = fidl_fuchsia_net_tun::DeviceConfig {
base: Some(fidl_fuchsia_net_tun::BaseDeviceConfig {
// Discard frame metadata. It is reported through the `read_frame`
// method.
report_metadata: Some(false),
min_tx_buffer_length: None,
..Default::default()
}),
// Blocking will cause the read and write frame methods to block. See
// the documentation on fuchsia.net.tun/DeviceConfig for more details.
blocking: Some(true),
..Default::default()
};
let port_config = fidl_fuchsia_net_tun::DevicePortConfig {
base: Some(fidl_fuchsia_net_tun::BasePortConfig {
// The port identifier that we'll use to refer to this port.
id: Some(PORT_ID),
// Device MTU reported to Netstack.
mtu: Some(1500),
// The frame types we're going to accept. TAP and TUN examples will
// request different frame types.
rx_types: Some(rx_types),
tx_types: Some(tx_types),
..Default::default()
}),
// Create the port with the link online signal.
online: Some(true),
// Use the MAC requested by the caller. TAP-like devices require a MAC
// address, while TUN-like devices don't.
mac,
..Default::default()
};
(device_config, port_config)
}
/// An example of configuring and operating a TAP-like interface using
/// fuchsia.net.tun.
#[fuchsia_async::run_singlethreaded(test)]
async fn tap_like_over_network_tun() {
// Connect to the ambient fuchsia.net.tun/Control service.
let tun =
fuchsia_component::client::connect_to_protocol::<fidl_fuchsia_net_tun::ControlMarker>()
.expect("failed to connect to service");
// Define the tun device configuration we want to use.
// For a TAP-like device, the frame type must be Ethernet, and we must
// provide a MAC address for the virtual interface.
let (device_config, port_config) = new_device_and_port_config(
vec![fidl_fuchsia_hardware_network::FrameType::Ethernet],
Some(CONFIG_FOR_TAP_LIKE.alice_mac),
);
// Request device creation.
let (tun_device, server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_net_tun::DeviceMarker>()
.expect("failed to create device endpoints");
let () = tun.create_device(&device_config, server_end).expect("failed to create device");
// Add a port.
let (tun_port, server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_net_tun::PortMarker>()
.expect("failed to create port endpoints");
let () = tun_device.add_port(&port_config, server_end).expect("failed to add port");
// Install the interface in the stack.
//
// _device_control must be kept alive because it encodes the device
// lifetime.
let (_device_control, control, interface_id) =
helpers::create_interface(&tun_device, &tun_port).await;
// Add an IPv4 address.
let () = helpers::add_interface_address(
&control,
interface_id,
CONFIG_FOR_TAP_LIKE.ip_layer.alice_subnet.clone(),
)
.await;
// Wait for Netstack to report the interface online. Netstack may drop
// frames if they're sent before the online signal is observed. This is
// necessary to prevent this test from flaking since we're only going to
// send a single echo request.
let () = helpers::wait_interface_online(interface_id).await;
// Now we can send frames. In this example we'll send an ICMP echo request
// and wait for the Netstack to respond.
let () = tun_device
.write_frame(&fidl_fuchsia_net_tun::Frame {
port: Some(PORT_ID),
frame_type: Some(fidl_fuchsia_hardware_network::FrameType::Ethernet),
data: Some(helpers::bob_pings_alice_for_tap_like(&CONFIG_FOR_TAP_LIKE)),
meta: None,
..Default::default()
})
.await
.expect("write_frame FIDL error")
.map_err(fuchsia_zircon::Status::from_raw)
.expect("write_frame failed");
// Read frames until we see the echo response.
loop {
let fidl_fuchsia_net_tun::Frame {
// Port over which the frame was sent.
port,
// The frame's type will always be Ethernet in this example. If multiple
// frame types are supported (like IPv4 and IPv6, for example) that
// information will be here.
frame_type,
// Frame payload.
data,
// Metadata associated with the frame.
meta: _,
..
} = tun_device
.read_frame()
.await
.expect("read_frame FIDL error")
.map_err(fuchsia_zircon::Status::from_raw)
.expect("read_frame failed");
let data = data.expect("received Frame with missing data field");
assert_eq!(port, Some(PORT_ID));
assert_eq!(frame_type, Some(fidl_fuchsia_hardware_network::FrameType::Ethernet));
match helpers::get_icmp_response_for_tap_like(&data[..]) {
None => (),
Some(helpers::ParsedFrame::ArpRequest { target_ip, sender_mac, sender_ip }) => {
// When we get an ARP request for bob's IP, we need to send an
// ARP response back in order for the ICMP echo response to come
// in.
if target_ip == CONFIG_FOR_TAP_LIKE.ip_layer.bob_ip {
assert_eq!(sender_mac, CONFIG_FOR_TAP_LIKE.alice_mac);
assert_eq!(sender_ip, CONFIG_FOR_TAP_LIKE.ip_layer.alice_subnet.addr);
let () = tun_device
.write_frame(&fidl_fuchsia_net_tun::Frame {
port: Some(PORT_ID),
frame_type: Some(fidl_fuchsia_hardware_network::FrameType::Ethernet),
data: Some(helpers::build_bob_arp_response(&CONFIG_FOR_TAP_LIKE)),
meta: None,
..Default::default()
})
.await
.expect("write_frame FIDL error")
.map_err(fuchsia_zircon::Status::from_raw)
.expect("write_frame failed");
}
}
Some(helpers::ParsedFrame::IcmpEchoResponse { src_mac, dst_mac, src_ip, dst_ip }) => {
assert_eq!(src_mac, CONFIG_FOR_TAP_LIKE.alice_mac);
assert_eq!(dst_mac, CONFIG_FOR_TAP_LIKE.bob_mac);
assert_eq!(src_ip, CONFIG_FOR_TAP_LIKE.ip_layer.alice_subnet.addr);
assert_eq!(dst_ip, CONFIG_FOR_TAP_LIKE.ip_layer.bob_ip);
break;
}
}
}
}
/// An example of configuring and operating a TUN-like interface using
/// fuchsia.net.tun.
#[fuchsia_async::run_singlethreaded(test)]
async fn tun_like_over_network_tun() {
// Connect to the ambient fuchsia.net.tun/Control service.
let tun =
fuchsia_component::client::connect_to_protocol::<fidl_fuchsia_net_tun::ControlMarker>()
.expect("failed to connect to service");
// Define the tun device configuration we want to use. For a TUN-like
// device, the frame types must be IPv4 and IPv6, and we don't provide a MAC
// address.
let (device_config, port_config) = new_device_and_port_config(
vec![
fidl_fuchsia_hardware_network::FrameType::Ipv4,
fidl_fuchsia_hardware_network::FrameType::Ipv6,
],
None,
);
// Request device creation.
let (tun_device, server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_net_tun::DeviceMarker>()
.expect("failed to create device endpoints");
let () = tun.create_device(&device_config, server_end).expect("failed to create device");
// Add a port.
let (tun_port, server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_net_tun::PortMarker>()
.expect("failed to create port endpoints");
let () = tun_device.add_port(&port_config, server_end).expect("failed to add port");
// Install the interface in the stack.
//
// _device_control must be kept alive because it encodes the device
// lifetime.
let (_device_control, control, interface_id) =
helpers::create_interface(&tun_device, &tun_port).await;
// Add an IPv4 address.
let () = helpers::add_interface_address(
&control,
interface_id,
CONFIG_FOR_TUN_LIKE.alice_subnet.clone(),
)
.await;
// Wait for Netstack to report the interface online. Netstack may drop
// frames if they're sent before the online signal is observed. This is
// necessary to prevent this test from flaking since we're only going to
// send a single echo request.
let () = helpers::wait_interface_online(interface_id).await;
// Now we can send frames. In this example we'll send an ICMP echo request
// and wait for the Netstack to respond.
let () = tun_device
.write_frame(&fidl_fuchsia_net_tun::Frame {
port: Some(PORT_ID),
frame_type: Some(fidl_fuchsia_hardware_network::FrameType::Ipv4),
data: Some(helpers::bob_pings_alice_for_tun_like(&CONFIG_FOR_TUN_LIKE)),
meta: None,
..Default::default()
})
.await
.expect("write_frame FIDL error")
.map_err(fuchsia_zircon::Status::from_raw)
.expect("write_frame failed");
// Read frames until we see the echo response.
loop {
let fidl_fuchsia_net_tun::Frame {
// Port over which the frame was sent.
port,
// A TUN-like device can receive IPv4 or IPv6 frames, in this
// example we're going to be looking only into IPv4 packets.
frame_type,
// Frame payload.
data,
// Metadata associated with the frame.
meta: _,
..
} = tun_device
.read_frame()
.await
.expect("read_frame FIDL error")
.map_err(fuchsia_zircon::Status::from_raw)
.expect("read_frame failed");
assert_eq!(port, Some(PORT_ID));
match frame_type {
Some(fidl_fuchsia_hardware_network::FrameType::Ipv4) => (),
Some(fidl_fuchsia_hardware_network::FrameType::Ipv6) => continue,
unexpected => {
panic!("received unexpected frame_type: {:?}", unexpected);
}
}
let data = data.expect("received Frame with missing data field");
if let Some((src_ip, dst_ip)) = helpers::get_icmp_response_for_tun_like(&data[..]) {
assert_eq!(src_ip, CONFIG_FOR_TUN_LIKE.alice_subnet.addr);
assert_eq!(dst_ip, CONFIG_FOR_TUN_LIKE.bob_ip);
break;
}
}
}
/// This module contains some helpers to remove noise from the examples in this
/// crate.
mod helpers {
use fidl::endpoints::Proxy as _;
use net_types::{
ethernet::Mac,
ip::{Ipv4, Ipv4Addr},
};
use packet::{InnerPacketBuilder as _, ParsablePacket as _, Serializer as _};
use packet_formats::{
arp::{ArpOp, ArpPacket, ArpPacketBuilder},
ethernet::{
EtherType, EthernetFrame, EthernetFrameBuilder, EthernetFrameLengthCheck,
ETHERNET_MIN_BODY_LEN_NO_TAG,
},
icmp::{IcmpEchoRequest, IcmpPacketBuilder, IcmpParseArgs, IcmpUnusedCode, Icmpv4Packet},
ip::Ipv4Proto,
ipv4::{Ipv4Header as _, Ipv4Packet, Ipv4PacketBuilder},
};
fn ip_v4(fidl_ip: fidl_fuchsia_net::IpAddress) -> Ipv4Addr {
match fidl_ip {
fidl_fuchsia_net::IpAddress::Ipv4(ip) => Ipv4Addr::new(ip.addr),
fidl_fuchsia_net::IpAddress::Ipv6(v6) => {
panic!("can't convert from FIDL IPv6 {:?}", v6)
}
}
}
const fn mac(fidl_mac: fidl_fuchsia_net::MacAddress) -> Mac {
Mac::new(fidl_mac.octets)
}
fn fidl_mac(mac: Mac) -> fidl_fuchsia_net::MacAddress {
fidl_fuchsia_net::MacAddress { octets: mac.bytes() }
}
fn fidl_ip(ip_v4: Ipv4Addr) -> fidl_fuchsia_net::IpAddress {
fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address {
addr: ip_v4.ipv4_bytes(),
})
}
const ECHO_PAYLOAD: [u8; 4] = [1, 2, 3, 4];
const ICMP_ID: u16 = 1;
const ICMP_SEQNUM: u16 = 1;
/// Creates the ICMP bytes for bob pinging alice to use in TAP-like
/// examples.
pub(super) fn bob_pings_alice_for_tap_like(config: &super::EthernetLayerConfig) -> Vec<u8> {
packet::Buf::new(&mut bob_pings_alice_for_tun_like(&config.ip_layer)[..], ..)
.encapsulate(EthernetFrameBuilder::new(
mac(config.bob_mac),
mac(config.alice_mac),
EtherType::Ipv4,
ETHERNET_MIN_BODY_LEN_NO_TAG,
))
.serialize_vec_outer()
.expect("serialization failed")
.as_ref()
.to_vec()
}
/// Creates the ICMP bytes for bob pinging alice to use in TUN-like
/// examples.
pub(super) fn bob_pings_alice_for_tun_like(config: &super::IpLayerConfig) -> Vec<u8> {
let alice_ip = ip_v4(config.alice_subnet.addr);
let bob_ip = ip_v4(config.bob_ip);
packet::Buf::new(&mut ECHO_PAYLOAD.to_vec()[..], ..)
.encapsulate(IcmpPacketBuilder::<Ipv4, _>::new(
bob_ip,
alice_ip,
IcmpUnusedCode,
IcmpEchoRequest::new(ICMP_ID, ICMP_SEQNUM),
))
.encapsulate(Ipv4PacketBuilder::new(bob_ip, alice_ip, 1, Ipv4Proto::Icmp))
.serialize_vec_outer()
.expect("serialization failed")
.as_ref()
.to_vec()
}
/// A parsed frame for a tap-like interface.
pub(super) enum ParsedFrame {
/// An ARP request was observed, an ARP response should be sent.
ArpRequest {
target_ip: fidl_fuchsia_net::IpAddress,
sender_mac: fidl_fuchsia_net::MacAddress,
sender_ip: fidl_fuchsia_net::IpAddress,
},
/// An ICMP echo response was observed.
IcmpEchoResponse {
src_mac: fidl_fuchsia_net::MacAddress,
dst_mac: fidl_fuchsia_net::MacAddress,
src_ip: fidl_fuchsia_net::IpAddress,
dst_ip: fidl_fuchsia_net::IpAddress,
},
}
/// Attempts to parse an ICMP echo response in an Ethernet frame contained
/// in `data`.
///
/// Returns `None` if the frame is valid but it's not an ICMP response.
///
/// Otherwise, returns either a [`ParsedFrame::IcmpEchoResponse`] with the
/// echo response or a [`ParsedFrame::ArpRequest`] indicating that an ARP
/// response must be sent for LL resolution.
///
/// # Panics
///
/// Panics if the frame can't be parsed.
pub(super) fn get_icmp_response_for_tap_like(data: &[u8]) -> Option<ParsedFrame> {
let mut bv = data;
let ethernet = EthernetFrame::parse(&mut bv, EthernetFrameLengthCheck::NoCheck)
.expect("failed to parse Ethernet frame");
let src_mac = fidl_mac(ethernet.src_mac());
let dst_mac = fidl_mac(ethernet.dst_mac());
match ethernet.ethertype() {
Some(EtherType::Ipv4) => get_icmp_response_for_tun_like(bv).map(|(src_ip, dst_ip)| {
ParsedFrame::IcmpEchoResponse { src_mac, dst_mac, src_ip, dst_ip }
}),
Some(EtherType::Arp) => {
let arp = ArpPacket::<_, Mac, Ipv4Addr>::parse(&mut bv, ())
.expect("failed to parse ARP packet");
match arp.operation() {
ArpOp::Request => Some(ParsedFrame::ArpRequest {
target_ip: fidl_ip(arp.target_protocol_address()),
sender_mac: fidl_mac(arp.sender_hardware_address()),
sender_ip: fidl_ip(arp.sender_protocol_address()),
}),
ArpOp::Response | ArpOp::Other(_) => None,
}
}
_ => None,
}
}
/// Attempts to parse an ICMP echo response in an Ethernet frame contained
/// in `data`.
///
/// Returns `None` if the frame is valid but it's not an ICMP response.
///
/// Otherwise, returns the `(src_ip, dst_ip)` addressing tuple.
///
/// Note that this function is provided as a support to the examples in this
/// crate and it makes assumptions that may not be valid in a production
/// environment, such as assuming that no IP fragmentation happens, and the
/// ICMP echo responses are not validated.
///
/// # Panics
///
/// Panics if the frame can't be parsed.
pub(super) fn get_icmp_response_for_tun_like(
data: &[u8],
) -> Option<(fidl_fuchsia_net::IpAddress, fidl_fuchsia_net::IpAddress)> {
let mut bv = data;
let ipv4 = Ipv4Packet::parse(&mut bv, ()).expect("failed to parse IPv4");
if ipv4.proto() != Ipv4Proto::Icmp {
return None;
}
let src_ip = ipv4.src_ip();
let dst_ip = ipv4.dst_ip();
let icmp = Icmpv4Packet::parse(&mut bv, IcmpParseArgs::new(src_ip, dst_ip))
.expect("failed to parse ICMP");
if let Icmpv4Packet::EchoReply(_echo) = icmp {
Some((fidl_ip(src_ip), fidl_ip(dst_ip)))
} else {
None
}
}
/// Creates an ARP response from bob to alice for use in tap-like tests.
pub(super) fn build_bob_arp_response(config: &super::EthernetLayerConfig) -> Vec<u8> {
ArpPacketBuilder::new(
ArpOp::Response,
mac(config.bob_mac),
ip_v4(config.ip_layer.bob_ip),
mac(config.alice_mac),
ip_v4(config.ip_layer.alice_subnet.addr),
)
.into_serializer()
.encapsulate(EthernetFrameBuilder::new(
mac(config.bob_mac),
mac(config.alice_mac),
EtherType::Arp,
ETHERNET_MIN_BODY_LEN_NO_TAG,
))
.serialize_vec_outer()
.expect("serialization failed")
.as_ref()
.to_vec()
}
/// Waits for interface with ID `interface_id` to come online.
pub(super) async fn wait_interface_online(interface_id: u64) {
let interface_state = fuchsia_component::client::connect_to_protocol::<
fidl_fuchsia_net_interfaces::StateMarker,
>()
.expect("failed to connect to interfaces state service");
fidl_fuchsia_net_interfaces_ext::wait_interface_with_id(
fidl_fuchsia_net_interfaces_ext::event_stream_from_state(
&interface_state,
fidl_fuchsia_net_interfaces_ext::IncludedAddresses::OnlyAssigned,
)
.expect("failed to create event stream"),
&mut fidl_fuchsia_net_interfaces_ext::InterfaceState::<()>::Unknown(interface_id),
|iface| iface.properties.online.then_some(()),
)
.await
.expect("failed to wait for interface online")
}
/// Installs the provided port on the stack.
pub(super) async fn create_interface(
tun_device: &fidl_fuchsia_net_tun::DeviceProxy,
tun_port: &fidl_fuchsia_net_tun::PortProxy,
) -> (
fidl_fuchsia_net_interfaces_admin::DeviceControlProxy,
fidl_fuchsia_net_interfaces_ext::admin::Control,
u64,
) {
// Get the information Installer needs to install the device.
let (network_device, port_id) = {
let (network_device, netdevice_server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_hardware_network::DeviceMarker>()
.expect("failed to create netdevice proxy");
let (port, port_server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_hardware_network::PortMarker>()
.expect("failed to create port proxy");
let () = tun_device.get_device(netdevice_server_end).expect("get_device failed");
let () = tun_port.get_port(port_server_end).expect("get_port failed");
let port_id = port.get_info().await.expect("get_info failed").id.expect("missing id");
let network_device: fidl::endpoints::ClientEnd<_> = network_device
.into_channel()
.expect("failed to get inner channel")
.into_zx_channel()
.into();
(network_device, port_id)
};
// Connect to Installer and install the device.
let installer = fuchsia_component::client::connect_to_protocol::<
fidl_fuchsia_net_interfaces_admin::InstallerMarker,
>()
.expect("failed to connect to installer");
let device_control = {
let (control, server_end) = fidl::endpoints::create_proxy::<
fidl_fuchsia_net_interfaces_admin::DeviceControlMarker,
>()
.expect("create proxy");
let () = installer
.install_device(network_device, server_end)
.expect("install_device failed");
control
};
let control = {
let (control, server_end) =
fidl_fuchsia_net_interfaces_ext::admin::Control::create_endpoints()
.expect("create endpoints");
let () = device_control
.create_interface(
&port_id,
server_end,
&fidl_fuchsia_net_interfaces_admin::Options::default(),
)
.expect("create_interface failed");
control
};
// Enable the interface.
let did_enable = control.enable().await.expect("enable FIDL error").expect("enable failed");
assert!(did_enable);
// Retrieve the interface ID.
let interface_id = control.get_id().await.expect("get_id failed");
(device_control, control, interface_id)
}
/// Adds an address to an installed interface.
pub(super) async fn add_interface_address(
control: &fidl_fuchsia_net_interfaces_ext::admin::Control,
interface_id: u64,
subnet: fidl_fuchsia_net::Subnet,
) {
let (address_state_provider, server_end) = fidl::endpoints::create_proxy::<
fidl_fuchsia_net_interfaces_admin::AddressStateProviderMarker,
>()
.expect("create proxy");
// AddressStateProvider allows us to tie the lifetime of the address to
// this proxy, opt out by detaching.
let () = address_state_provider.detach().expect("detach failed");
let () = control
.add_address(
&subnet,
&fidl_fuchsia_net_interfaces_admin::AddressParameters::default(),
server_end,
)
.expect("add_address failed");
// Wait for the address to be assigned.
let () = fidl_fuchsia_net_interfaces_ext::admin::wait_assignment_state(
&mut fidl_fuchsia_net_interfaces_ext::admin::assignment_state_stream(
address_state_provider,
),
fidl_fuchsia_net_interfaces::AddressAssignmentState::Assigned,
)
.await
.expect("failed to observe assigned address");
// NB: Adding an address does not add the subnet route, we still have to
// add that so we can exchange frames.
let stack =
fuchsia_component::client::connect_to_protocol::<fidl_fuchsia_net_stack::StackMarker>()
.expect("failed to connect to stack");
let () = stack
.add_forwarding_entry(&fidl_fuchsia_net_stack::ForwardingEntry {
subnet: fidl_fuchsia_net_ext::apply_subnet_mask(subnet),
device_id: interface_id,
next_hop: None,
metric: 0,
})
.await
.expect("add_forwarding_entry FIDL error")
.expect("add_forwarding_entry failed");
}
}