blob: 51fdfa5f5197c480ccb9d4206f5665b257c83620 [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 anyhow::{Context as _, Error};
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"),
};
/// Creates a new [`fidl_fuchsia_net_tun::DeviceConfig`] using for examples
/// using the provided `frame_type` and `mac`.
fn new_device_config(
frame_types: Vec<fidl_fuchsia_hardware_network::FrameType>,
mac: Option<fidl_fuchsia_net::MacAddress>,
) -> fidl_fuchsia_net_tun::DeviceConfig {
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();
fidl_fuchsia_net_tun::DeviceConfig {
base: Some(fidl_fuchsia_net_tun::BaseConfig {
// 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),
// Discard frame metadata. It is reported through the `read_frame`
// method.
report_metadata: Some(false),
min_tx_buffer_length: None,
..fidl_fuchsia_net_tun::BaseConfig::EMPTY
}),
// Create the device with the link online signal.
online: Some(true),
// 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),
// Use the MAC requested by the caller. TAP-like devices require a MAC
// address, while TUN-like devices don't.
mac,
..fidl_fuchsia_net_tun::DeviceConfig::EMPTY
}
}
/// 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() -> Result<(), Error> {
// Connect to the ambient fuchsia.net.tun/Control service.
let tun =
fuchsia_component::client::connect_to_service::<fidl_fuchsia_net_tun::ControlMarker>()
.context("failed to connect to service")?;
let (tun_device, server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_net_tun::DeviceMarker>()
.context("failed to create device endpoints")?;
// 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 config = new_device_config(
vec![fidl_fuchsia_hardware_network::FrameType::Ethernet],
Some(CONFIG_FOR_TAP_LIKE.alice_mac),
);
// Request device creation.
let () = tun.create_device(config, server_end).context("failed to create device")?;
// Get the Netstack ends of the tun Ethernet device.
let (network_device, netdevice_server_end) =
fidl::endpoints::create_endpoints::<fidl_fuchsia_hardware_network::DeviceMarker>()
.context("failed to create netdevice endpoints")?;
let (mac, mac_server_end) =
fidl::endpoints::create_endpoints::<fidl_fuchsia_hardware_network::MacAddressingMarker>()
.context("failed to create netdevice endpoints")?;
let () = tun_device
.connect_protocols(fidl_fuchsia_net_tun::Protocols {
network_device: Some(netdevice_server_end),
mac_addressing: Some(mac_server_end),
..fidl_fuchsia_net_tun::Protocols::EMPTY
})
.context("failed to connect protocols")?;
// Connect to Netstack and install the device.
let stack =
fuchsia_component::client::connect_to_service::<fidl_fuchsia_net_stack::StackMarker>()
.context("failed to connect to stack")?;
let interface_id = stack
.add_interface(
// Interface configuration.
fidl_fuchsia_net_stack::InterfaceConfig {
name: Some("tap-like".to_string()),
topopath: Some("/fake-topopath".to_string()),
metric: Some(100),
..fidl_fuchsia_net_stack::InterfaceConfig::EMPTY
},
// The device definition tells Netstack this is an Ethernet device
// and gives it handles to access the data plane and the MAC
// addressing control plane.
&mut fidl_fuchsia_net_stack::DeviceDefinition::Ethernet(
fidl_fuchsia_net_stack::EthernetDeviceDefinition { network_device, mac },
),
)
.await
.context("add_interface FIDL error")?
.map_err(|e: fidl_fuchsia_net_stack::Error| {
anyhow::anyhow!("add_interface failed: {:?}", e)
})?;
// Enable the interface.
let () = stack
.enable_interface(interface_id)
.await
.context("enable_interface FIDL error")?
.map_err(|e: fidl_fuchsia_net_stack::Error| {
anyhow::anyhow!("enable_interface failed: {:?}", e)
})?;
// Add an IPv4 address to it.
let () = stack
.add_interface_address(interface_id, &mut CONFIG_FOR_TAP_LIKE.ip_layer.alice_subnet.clone())
.await
.context("add_interface_address FIDL error")?
.map_err(|e: fidl_fuchsia_net_stack::Error| {
anyhow::anyhow!("add_interface_address failed: {:?}", e)
})?;
// 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
.context("failed to observe interface online")?;
// 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 {
frame_type: Some(fidl_fuchsia_hardware_network::FrameType::Ethernet),
data: Some(helpers::bob_pings_alice_for_tap_like(&CONFIG_FOR_TAP_LIKE)),
meta: None,
..fidl_fuchsia_net_tun::Frame::EMPTY
})
.await
.context("write_frame FIDL error")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("write_frame failed")?;
// Read frames until we see the echo response.
loop {
let fidl_fuchsia_net_tun::Frame {
// 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
.context("read_frame FIDL error")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("read_frame failed")?;
let data = data.ok_or_else(|| anyhow::anyhow!("received Frame with missing data field"))?;
assert_eq!(frame_type, Some(fidl_fuchsia_hardware_network::FrameType::Ethernet));
match helpers::get_icmp_response_for_tap_like(&data[..])
.context("failed to parse ICMP response")?
{
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 {
frame_type: Some(fidl_fuchsia_hardware_network::FrameType::Ethernet),
data: Some(helpers::build_bob_arp_response(&CONFIG_FOR_TAP_LIKE)),
meta: None,
..fidl_fuchsia_net_tun::Frame::EMPTY
})
.await
.context("write_frame FIDL error")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("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 Ok(());
}
}
}
}
/// 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() -> Result<(), Error> {
// Connect to the ambient fuchsia.net.tun/Control service.
let tun =
fuchsia_component::client::connect_to_service::<fidl_fuchsia_net_tun::ControlMarker>()
.context("failed to connect to service")?;
let (tun_device, server_end) =
fidl::endpoints::create_proxy::<fidl_fuchsia_net_tun::DeviceMarker>()
.context("failed to create device endpoints")?;
// 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 config = new_device_config(
vec![
fidl_fuchsia_hardware_network::FrameType::Ipv4,
fidl_fuchsia_hardware_network::FrameType::Ipv6,
],
None,
);
// Request device creation.
let () = tun.create_device(config, server_end).context("failed to create device")?;
// Get the Netstack end of the tun IPv4/IPv6 device.
let (network_device, netdevice_server_end) =
fidl::endpoints::create_endpoints::<fidl_fuchsia_hardware_network::DeviceMarker>()
.context("failed to create netdevice endpoints")?;
let () = tun_device
.connect_protocols(fidl_fuchsia_net_tun::Protocols {
network_device: Some(netdevice_server_end),
mac_addressing: None,
..fidl_fuchsia_net_tun::Protocols::EMPTY
})
.context("failed to connect protocols")?;
// Connect to Netstack and install the device.
let stack =
fuchsia_component::client::connect_to_service::<fidl_fuchsia_net_stack::StackMarker>()
.context("failed to connect to stack")?;
let interface_id = stack
.add_interface(
// Interface configuration.
fidl_fuchsia_net_stack::InterfaceConfig {
name: Some("tun-like".to_string()),
topopath: Some("/fake-topopath".to_string()),
metric: Some(100),
..fidl_fuchsia_net_stack::InterfaceConfig::EMPTY
},
// The device definition tells Netstack this is a TUN-like device
// operating at the IP layer and gives it handles to access the data
// plane.
&mut fidl_fuchsia_net_stack::DeviceDefinition::Ip(network_device),
)
.await
.context("add_interface FIDL error")?
.map_err(|e: fidl_fuchsia_net_stack::Error| {
anyhow::anyhow!("add_interface failed: {:?}", e)
})?;
// Enable the interface.
let () = stack
.enable_interface(interface_id)
.await
.context("enable_interface FIDL error")?
.map_err(|e: fidl_fuchsia_net_stack::Error| {
anyhow::anyhow!("enable_interface failed: {:?}", e)
})?;
// Add an IPv4 address to it.
let () = stack
.add_interface_address(interface_id, &mut CONFIG_FOR_TUN_LIKE.alice_subnet.clone())
.await
.context("add_interface_address FIDL error")?
.map_err(|e: fidl_fuchsia_net_stack::Error| {
anyhow::anyhow!("add_interface_address failed: {:?}", e)
})?;
// 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
.context("failed to observe interface online")?;
// 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 {
frame_type: Some(fidl_fuchsia_hardware_network::FrameType::Ipv4),
data: Some(helpers::bob_pings_alice_for_tun_like(&CONFIG_FOR_TUN_LIKE)),
meta: None,
..fidl_fuchsia_net_tun::Frame::EMPTY
})
.await
.context("write_frame FIDL error")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("write_frame failed")?;
// Read frames until we see the echo response.
loop {
let fidl_fuchsia_net_tun::Frame {
// 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
.context("read_frame FIDL error")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("read_frame failed")?;
match frame_type {
Some(fidl_fuchsia_hardware_network::FrameType::Ipv4) => (),
Some(fidl_fuchsia_hardware_network::FrameType::Ipv6) => continue,
unexpected => {
break Err(anyhow::anyhow!("received unexpected frame_type: {:?}", unexpected))
}
}
let data = data.ok_or_else(|| anyhow::anyhow!("received Frame with missing data field"))?;
if let Some((src_ip, dst_ip)) = helpers::get_icmp_response_for_tun_like(&data[..])
.context("failed to parse ICMP response")?
{
assert_eq!(src_ip, CONFIG_FOR_TUN_LIKE.alice_subnet.addr);
assert_eq!(dst_ip, CONFIG_FOR_TUN_LIKE.bob_ip);
break Ok(());
}
}
}
/// This module contains some helpers to remove noise from the examples in this
/// crate.
mod helpers {
use anyhow::{Context as _, Error};
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},
icmp::{IcmpEchoRequest, IcmpPacketBuilder, IcmpParseArgs, IcmpUnusedCode, Icmpv4Packet},
ip::IpProto,
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,
))
.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, &[u8], _>::new(
bob_ip,
alice_ip,
IcmpUnusedCode,
IcmpEchoRequest::new(ICMP_ID, ICMP_SEQNUM),
))
.encapsulate(Ipv4PacketBuilder::new(bob_ip, alice_ip, 1, IpProto::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 `Err` if the frame can't be parsed, `Ok(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.
pub(super) fn get_icmp_response_for_tap_like(
data: &[u8],
) -> Result<Option<ParsedFrame>, Error> {
let mut bv = data;
let ethernet = EthernetFrame::parse(&mut bv, EthernetFrameLengthCheck::NoCheck)
.context("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(|r| {
r.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, ())
.context("failed to parse ARP packet")?;
match arp.operation() {
ArpOp::Request => Ok(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(_) => Ok(None),
}
}
_ => Ok(None),
}
}
/// Attempts to parse an ICMP echo response in an Ethernet frame contained
/// in `data`.
///
/// Returns `Err` if the frame can't be parsed, `Ok(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.
pub(super) fn get_icmp_response_for_tun_like(
data: &[u8],
) -> Result<Option<(fidl_fuchsia_net::IpAddress, fidl_fuchsia_net::IpAddress)>, Error> {
let mut bv = data;
let ipv4 = Ipv4Packet::parse(&mut bv, ()).context("failed to parse IPv4")?;
if ipv4.proto() != IpProto::Icmp {
return Ok(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))
.context("failed to parse ICMP")?;
if let Icmpv4Packet::EchoReply(_echo) = icmp {
Ok(Some((fidl_ip(src_ip), fidl_ip(dst_ip))))
} else {
Ok(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,
))
.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) -> Result<(), Error> {
let interface_state = fuchsia_component::client::connect_to_service::<
fidl_fuchsia_net_interfaces::StateMarker,
>()
.context("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)
.context("failed to create event stream")?,
&mut fidl_fuchsia_net_interfaces_ext::InterfaceState::Unknown(interface_id),
|&fidl_fuchsia_net_interfaces_ext::Properties {
id: _,
addresses: _,
online,
device_class: _,
has_default_ipv4_route: _,
has_default_ipv6_route: _,
name: _,
}| {
if online {
Some(())
} else {
None
}
},
)
.await
.context("failed to wait for interface online")
}
}