blob: 46da6d58bb5c0308f48b0d15b2a518beaa88b2ac [file] [log] [blame]
// Copyright 2023 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)]
// Needed for invocations of the `assert_data_tree` macro.
#![recursion_limit = "256"]
use std::{collections::HashMap, convert::TryFrom as _, num::NonZeroU64, time::Duration};
use assert_matches::assert_matches;
use fidl_fuchsia_net_filter as fnet_filter;
use fidl_fuchsia_net_filter_ext as fnet_filter_ext;
use fidl_fuchsia_posix_socket as fposix_socket;
use net_declare::{fidl_mac, fidl_subnet, std_ip_v4, std_ip_v6};
use net_types::{
ip::{IpAddress, IpInvariant, IpVersion, Ipv4, Ipv6},
AddrAndPortFormatter, Witness as _,
};
use netstack_testing_common::{
constants, get_inspect_data,
realms::{Netstack3, TestSandboxExt as _},
};
use netstack_testing_macros::netstack_test;
use packet_formats::ethernet::testutil::ETHERNET_HDR_LEN_NO_TAG;
use test_case::test_case;
enum TcpSocketState {
Unbound,
Bound,
Listener,
Connected,
}
#[netstack_test]
#[test_case(TcpSocketState::Unbound; "unbound")]
#[test_case(TcpSocketState::Bound; "bound")]
#[test_case(TcpSocketState::Listener; "listener")]
#[test_case(TcpSocketState::Connected; "connected")]
async fn inspect_tcp_sockets<I: net_types::ip::Ip>(name: &str, socket_state: TcpSocketState) {
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let realm =
sandbox.create_netstack_realm::<Netstack3, _>(name).expect("failed to create realm");
let network = sandbox.create_network("net").await.expect("failed to create network");
let interfaces_state = realm
.connect_to_protocol::<fidl_fuchsia_net_interfaces::StateMarker>()
.expect("connect to protocol");
let dev = realm.join_network(&network, "dev").await.expect("join network");
let link_local =
netstack_testing_common::interfaces::wait_for_v6_ll(&interfaces_state, dev.id())
.await
.expect("wait for v6 link local");
let scope = dev.id().try_into().unwrap();
// Ensure ns3 has started and that there is a Socket to collect inspect data about.
const LOCAL_PORT: u16 = 8080;
const REMOTE_PORT: u16 = 9999;
const BACKLOG: u64 = 123;
let (domain, local_addr) = match (I::VERSION, &socket_state) {
(IpVersion::V4, TcpSocketState::Unbound) => (fposix_socket::Domain::Ipv4, None),
// NB: For bound sockets, use the any address, which guards against
// a regression where the any address is shown as "[NOT BOUND]".
(IpVersion::V4, TcpSocketState::Bound) => (
fposix_socket::Domain::Ipv4,
Some(std::net::SocketAddr::from((std_ip_v4!("0.0.0.0"), LOCAL_PORT))),
),
(IpVersion::V4, TcpSocketState::Listener) | (IpVersion::V4, TcpSocketState::Connected) => {
// NB: Ensure the address exists on the device so that we can bind
// to it. This only applies to IPv4 since IPv6 is using the link
// local address.
dev.add_address(fidl_subnet!("192.0.2.1/24")).await.expect("add address");
(
fposix_socket::Domain::Ipv4,
Some(std::net::SocketAddr::from((std_ip_v4!("192.0.2.1"), LOCAL_PORT))),
)
}
(IpVersion::V6, TcpSocketState::Unbound) => (fposix_socket::Domain::Ipv6, None),
// NB: For bound sockets, use the any address, which guards against
// a regression where the any address is shown as "[NOT BOUND]".
(IpVersion::V6, TcpSocketState::Bound) => (
fposix_socket::Domain::Ipv6,
Some(std::net::SocketAddr::from((std_ip_v6!("::"), LOCAL_PORT))),
),
(IpVersion::V6, TcpSocketState::Listener) | (IpVersion::V6, TcpSocketState::Connected) => (
fposix_socket::Domain::Ipv6,
Some(std::net::SocketAddr::from(std::net::SocketAddrV6::new(
link_local.into(),
LOCAL_PORT,
0,
scope,
))),
),
};
let tcp_socket = realm
.stream_socket(domain, fposix_socket::StreamSocketProtocol::Tcp)
.await
.expect("create TCP socket");
if let Some(local_addr) = local_addr {
tcp_socket.bind(&local_addr.into()).expect("bind");
}
match socket_state {
TcpSocketState::Unbound | TcpSocketState::Bound => {}
TcpSocketState::Listener => tcp_socket.listen(BACKLOG.try_into().unwrap()).expect("listen"),
TcpSocketState::Connected => {
let IpInvariant((default_subnet, peer_addr)) = I::map_ip(
(),
|()| {
IpInvariant((
fidl_subnet!("0.0.0.0/0"),
std::net::SocketAddr::from(std::net::SocketAddrV4::new(
std_ip_v4!("192.0.2.2"),
REMOTE_PORT,
)),
))
},
|()| {
IpInvariant((
fidl_subnet!("::/0"),
std::net::SocketAddr::from(std::net::SocketAddrV6::new(
std_ip_v6!("2001:db8::2"),
REMOTE_PORT,
0,
0,
)),
))
},
);
// Add a default route to the device, allowing the connection to
// begin. Note that because there is no peer end to accept the
// connection we setup the socket as non-blocking, and give a large
// connection timeout. This ensures the socket is still a "pending
// connection" when we fetch the inspect data.
dev.add_subnet_route(default_subnet).await.expect("add_default_route");
tcp_socket.set_nonblocking(true).expect("set nonblocking");
const DAY: Duration = Duration::from_secs(60 * 60 * 24);
tcp_socket.set_tcp_user_timeout(Some(DAY)).expect("set timeout");
let err = assert_matches!(tcp_socket.connect(&peer_addr.into()), Err(e) => e);
assert_eq!(err.raw_os_error(), Some(libc::EINPROGRESS));
}
}
let data =
get_inspect_data(&realm, "netstack", "root", constants::inspect::DEFAULT_INSPECT_TREE_NAME)
.await
.expect("inspect data should be present");
// Debug print the tree to make debugging easier in case of failures.
println!("Got inspect data: {:#?}", data);
// NB: The sockets are keyed by an opaque debug identifier; get that here.
let sockets = data.get_child("Sockets").unwrap();
assert_eq!(sockets.children.len(), 1);
let sock_name = sockets.children[0].name.clone();
match (I::VERSION, socket_state) {
(IpVersion::V4, TcpSocketState::Unbound) => {
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Sockets": {
sock_name => {
LocalAddress: "[NOT BOUND]",
RemoteAddress: "[NOT CONNECTED]",
TransportProtocol: "TCP",
NetworkProtocol: "IPv4",
},
}
})
}
(IpVersion::V4, TcpSocketState::Bound) => {
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Sockets": {
sock_name => {
LocalAddress: format!("0.0.0.0:{LOCAL_PORT}"),
RemoteAddress: "[NOT CONNECTED]",
TransportProtocol: "TCP",
NetworkProtocol: "IPv4",
},
}
})
}
(IpVersion::V4, TcpSocketState::Listener) => {
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Sockets": {
sock_name => {
LocalAddress: format!("192.0.2.1:{LOCAL_PORT}"),
RemoteAddress: "[NOT CONNECTED]",
TransportProtocol: "TCP",
NetworkProtocol: "IPv4",
AcceptQueue: {
BacklogSize: BACKLOG,
NumPending: 0u64,
NumReady: 0u64,
Contents: "{}",
}
},
}
})
}
(IpVersion::V4, TcpSocketState::Connected) => {
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Sockets": {
sock_name => {
LocalAddress: format!("192.0.2.1:{LOCAL_PORT}"),
RemoteAddress: format!("192.0.2.2:{REMOTE_PORT}"),
TransportProtocol: "TCP",
NetworkProtocol: "IPv4",
State: "SynSent",
},
}
})
}
(IpVersion::V6, TcpSocketState::Unbound) => {
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Sockets": {
sock_name => {
LocalAddress: "[NOT BOUND]",
RemoteAddress: "[NOT CONNECTED]",
TransportProtocol: "TCP",
NetworkProtocol: "IPv6",
}
}
})
}
(IpVersion::V6, TcpSocketState::Bound) => {
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Sockets": {
sock_name => {
LocalAddress: format!("[::]:{LOCAL_PORT}"),
RemoteAddress: "[NOT CONNECTED]",
TransportProtocol: "TCP",
NetworkProtocol: "IPv6",
}
}
})
}
(IpVersion::V6, TcpSocketState::Listener) => {
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Sockets": {
sock_name => {
LocalAddress: format!("[{link_local}%{scope}]:{LOCAL_PORT}"),
RemoteAddress: "[NOT CONNECTED]",
TransportProtocol: "TCP",
NetworkProtocol: "IPv6",
AcceptQueue: {
BacklogSize: BACKLOG,
NumPending: 0u64,
NumReady: 0u64,
Contents: "{}",
}
}
}
})
}
(IpVersion::V6, TcpSocketState::Connected) => {
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Sockets": {
sock_name => {
LocalAddress: format!("[{link_local}%{scope}]:{LOCAL_PORT}"),
RemoteAddress: format!("[2001:db8::2]:{REMOTE_PORT}"),
TransportProtocol: "TCP",
NetworkProtocol: "IPv6",
State: "SynSent",
}
}
})
}
}
}
enum SocketState {
Bound,
Connected,
}
trait TestIpExt: net_types::ip::Ip {
const DOMAIN: fposix_socket::Domain;
}
impl TestIpExt for Ipv4 {
const DOMAIN: fposix_socket::Domain = fposix_socket::Domain::Ipv4;
}
impl TestIpExt for Ipv6 {
const DOMAIN: fposix_socket::Domain = fposix_socket::Domain::Ipv6;
}
#[netstack_test]
#[test_case(
fposix_socket::DatagramSocketProtocol::Udp, SocketState::Bound;
"udp_bound"
)]
#[test_case(
fposix_socket::DatagramSocketProtocol::IcmpEcho, SocketState::Bound;
"icmp_bound"
)]
#[test_case(
fposix_socket::DatagramSocketProtocol::Udp, SocketState::Connected;
"udp_connected"
)]
#[test_case(
fposix_socket::DatagramSocketProtocol::IcmpEcho, SocketState::Connected;
"icmp_connected"
)]
async fn inspect_datagram_sockets<I: net_types::ip::Ip + TestIpExt>(
name: &str,
proto: fposix_socket::DatagramSocketProtocol,
socket_state: SocketState,
) {
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let realm =
sandbox.create_netstack_realm::<Netstack3, _>(name).expect("failed to create realm");
// Ensure ns3 has started and that there is a Socket to collect inspect data about.
let socket = realm.datagram_socket(I::DOMAIN, proto).await.expect("create datagram socket");
const SRC_PORT: u16 = 1234;
const DST_PORT: u16 = 5678;
let addr = std::net::IpAddr::from(
match socket_state {
SocketState::Bound => I::UNSPECIFIED_ADDRESS,
SocketState::Connected => I::LOOPBACK_ADDRESS.get(),
}
.to_ip_addr(),
);
socket.bind(&std::net::SocketAddr::from((addr, SRC_PORT)).into()).expect("bind");
match socket_state {
SocketState::Connected => {
socket.connect(&std::net::SocketAddr::from((addr, DST_PORT)).into()).expect("connect");
}
SocketState::Bound => {}
}
let want_local = AddrAndPortFormatter::<_, _, I>::new(addr, SRC_PORT).to_string();
let want_remote = match socket_state {
SocketState::Connected => AddrAndPortFormatter::<_, _, I>::new(addr, DST_PORT).to_string(),
SocketState::Bound => "[NOT CONNECTED]".to_string(),
};
let want_proto = match proto {
fposix_socket::DatagramSocketProtocol::Udp => "UDP",
fposix_socket::DatagramSocketProtocol::IcmpEcho => "ICMP_ECHO",
};
let data =
get_inspect_data(&realm, "netstack", "root", constants::inspect::DEFAULT_INSPECT_TREE_NAME)
.await
.expect("inspect data should be present");
// Debug print the tree to make debugging easier in case of failures.
println!("Got inspect data: {:#?}", data);
// NB: The sockets are keyed by an opaque debug identifier.
let sockets = data.get_child("Sockets").unwrap();
assert_eq!(sockets.children.len(), 1);
let sock_name = sockets.children[0].name.clone();
diagnostics_assertions::assert_data_tree!(data, "root": contains {
Sockets: {
sock_name => {
LocalAddress: want_local,
RemoteAddress: want_remote,
TransportProtocol: want_proto,
NetworkProtocol: I::NAME,
},
}
})
}
#[netstack_test]
async fn inspect_routes(name: &str) {
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let realm =
sandbox.create_netstack_realm::<Netstack3, _>(name).expect("failed to create realm");
let interfaces_state = realm
.connect_to_protocol::<fidl_fuchsia_net_interfaces::StateMarker>()
.expect("failed to connect to fuchsia.net.interfaces/State");
let loopback_id = fidl_fuchsia_net_interfaces_ext::wait_interface(
fidl_fuchsia_net_interfaces_ext::event_stream_from_state(
&interfaces_state,
fidl_fuchsia_net_interfaces_ext::IncludedAddresses::OnlyAssigned,
)
.expect("failed to create event stream"),
&mut HashMap::<u64, fidl_fuchsia_net_interfaces_ext::PropertiesAndState<()>>::new(),
|if_map| {
if_map.values().find_map(
|fidl_fuchsia_net_interfaces_ext::PropertiesAndState {
properties:
fidl_fuchsia_net_interfaces_ext::Properties { device_class, id, .. },
state: (),
}| {
match device_class {
fidl_fuchsia_net_interfaces::DeviceClass::Loopback(
fidl_fuchsia_net_interfaces::Empty {},
) => Some(id.get()),
fidl_fuchsia_net_interfaces::DeviceClass::Device(_) => None,
}
},
)
},
)
.await
.expect("getting loopback id");
let data =
get_inspect_data(&realm, "netstack", "root", constants::inspect::DEFAULT_INSPECT_TREE_NAME)
.await
.expect("inspect data should be present");
// Debug print the tree to make debugging easier in case of failures.
println!("Got inspect data: {:#?}", data);
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Routes": {
"0": {
Destination: "127.0.0.0/8",
InterfaceId: loopback_id,
Gateway: "[NONE]",
Metric: 100u64,
MetricTracksInterface: true,
},
"1": {
Destination: "224.0.0.0/4",
InterfaceId: loopback_id,
Gateway: "[NONE]",
Metric: 100u64,
MetricTracksInterface: true,
},
"2": {
Destination: "::1/128",
InterfaceId: loopback_id,
Gateway: "[NONE]",
Metric: 100u64,
MetricTracksInterface: true,
},
"3": {
Destination: "ff00::/8",
InterfaceId: loopback_id,
Gateway: "[NONE]",
Metric: 100u64,
MetricTracksInterface: true,
},
}
})
}
#[netstack_test]
async fn inspect_devices(name: &str) {
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let network = sandbox.create_network("net").await.expect("failed to create network");
let realm =
sandbox.create_netstack_realm::<Netstack3, _>(name).expect("failed to create realm");
// Install netdevice device so that non-Loopback device Inspect properties can be asserted upon.
const NETDEV_NAME: &str = "test-eth";
let max_frame_size = netemul::DEFAULT_MTU
+ u16::try_from(ETHERNET_HDR_LEN_NO_TAG)
.expect("should fit ethernet header length in a u16");
let netdev = realm
.join_network_with(
&network,
"netdev-ep",
netemul::new_endpoint_config(max_frame_size, Some(fidl_mac!("02:00:00:00:00:01"))),
netemul::InterfaceConfig {
name: Some(NETDEV_NAME.into()),
metric: None,
dad_transmits: Some(u16::MAX),
},
)
.await
.expect("failed to join network with netdevice endpoint");
netdev
.add_address_and_subnet_route(fidl_subnet!("192.168.0.1/24"))
.await
.expect("configure address");
let data =
get_inspect_data(&realm, "netstack", "root", constants::inspect::DEFAULT_INSPECT_TREE_NAME)
.await
.expect("inspect data should be present");
// Debug print the tree to make debugging easier in case of failures.
println!("Got inspect data: {:#?}", data);
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Devices": {
"1": {
Name: "lo",
InterfaceId: 1u64,
AdminEnabled: true,
MTU: 65536u64,
Loopback: true,
IPv4: {
Addresses: {
"127.0.0.1/8": {
ValidUntil: "infinite",
}
}
},
IPv6: {
Addresses: {
"::1/128": {
ValidUntil: "infinite",
IsSlaac: false,
Deprecated: false,
Assigned: true,
}
}
},
Counters: {
Rx: {
TotalFrames: 0u64,
Malformed: 0u64,
Ipv4Delivered: 0u64,
Ipv6Delivered: 0u64,
},
Tx: {
TotalFrames: 0u64,
Sent: 0u64,
SendIpv4Frame: 0u64,
SendIpv6Frame: 0u64,
NoQueue: 0u64,
QueueFull: 0u64,
SerializeError: 0u64,
},
Ethernet: {
Rx: {
NoEthertype: 0u64,
NonLocalDstAddr: 0u64,
UnsupportedEthertype: 0u64,
},
},
}
},
"2": {
Name: NETDEV_NAME,
InterfaceId: 2u64,
AdminEnabled: true,
MTU: u64::from(netemul::DEFAULT_MTU),
Loopback: false,
IPv4: {
"Addresses": {
"192.168.0.1/24": {
ValidUntil: "infinite"
}
}
},
IPv6: {
"Addresses": {
"fe80::ff:fe00:1/64": {
ValidUntil: "infinite",
IsSlaac: true,
Deprecated: false,
// This will always be `false` because DAD will never complete; we set
// the number of DAD transmits to `u16::MAX` above.
Assigned: false,
}
}
},
NetworkDevice: {
MacAddress: "02:00:00:00:00:01",
PhyUp: true,
},
Counters: {
Rx: {
TotalFrames: 0u64,
Malformed: 0u64,
Ipv4Delivered: 0u64,
Ipv6Delivered: 0u64,
},
Tx: {
TotalFrames: diagnostics_assertions::AnyUintProperty,
Sent: diagnostics_assertions::AnyUintProperty,
SendIpv4Frame: diagnostics_assertions::AnyUintProperty,
SendIpv6Frame: diagnostics_assertions::AnyUintProperty,
NoQueue: 0u64,
QueueFull: 0u64,
SerializeError: 0u64,
},
Ethernet: {
Rx: {
NoEthertype: 0u64,
NonLocalDstAddr: 0u64,
UnsupportedEthertype: 0u64,
},
},
}
}
}
})
}
#[netstack_test]
async fn inspect_counters(name: &str) {
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let realm =
sandbox.create_netstack_realm::<Netstack3, _>(name).expect("failed to create realm");
// Send a packet over loopback to increment Tx and Rx count by 1.
let sender = realm
.datagram_socket(fposix_socket::Domain::Ipv4, fposix_socket::DatagramSocketProtocol::Udp)
.await
.expect("datagram socket creation failed");
let addr = net_declare::std_socket_addr!("127.0.0.1:8080");
let buf = [0; 8];
let bytes_sent = sender.send_to(&buf, &addr.into()).expect("socket send to failed");
assert_eq!(bytes_sent, buf.len());
let data =
get_inspect_data(&realm, "netstack", "root", constants::inspect::DEFAULT_INSPECT_TREE_NAME)
.await
.expect("inspect data should be present");
// Debug print the tree to make debugging easier in case of failures.
println!("Got inspect data: {:#?}", data);
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Counters": {
"Device": {
"Rx": {
TotalFrames: 1u64,
Malformed: 0u64,
Ipv4Delivered: 1u64,
Ipv6Delivered: 0u64,
},
"Tx": {
TotalFrames: 1u64,
Sent: 1u64,
SendIpv4Frame: 1u64,
SendIpv6Frame: 0u64,
NoQueue: 0u64,
QueueFull: 0u64,
SerializeError: 0u64,
},
"Ethernet": {
"Rx": {
NoEthertype: 0u64,
NonLocalDstAddr: 0u64,
UnsupportedEthertype: 0u64,
},
},
},
"Arp": {
"Rx": {
TotalPackets: 0u64,
Requests: 0u64,
Responses: 0u64,
Malformed: 0u64,
NonLocalDstAddr: 0u64,
},
"Tx": {
Requests: 0u64,
RequestsNonLocalSrcAddr: 0u64,
Responses: 0u64,
},
},
"NUD": {
"V4": {
IcmpDestUnreachableDropped: 0u64,
},
"V6": {
IcmpDestUnreachableDropped: 0u64,
},
},
"ICMP": {
"V4": {
"Rx": {
EchoRequest: 0u64,
EchoReply: 0u64,
TimestampRequest: 0u64,
DestUnreachable: 0u64,
TimeExceeded: 0u64,
ParameterProblem: 0u64,
PacketTooBig: 0u64,
Error: 0u64,
ErrorDeliveredToTransportLayer: 0u64,
ErrorDeliveredToSocket: 0u64,
},
"Tx": {
Reply: 0u64,
AddressUnreachable: 0u64,
ProtocolUnreachable: 0u64,
PortUnreachable: 0u64,
NetUnreachable: 0u64,
TtlExpired: 0u64,
PacketTooBig: 0u64,
ParameterProblem: 0u64,
DestUnreachable: 0u64,
Error: 0u64,
},
},
"V6": {
"Rx": {
EchoRequest: 0u64,
EchoReply: 0u64,
TimestampRequest: 0u64,
DestUnreachable: 0u64,
TimeExceeded: 0u64,
ParameterProblem: 0u64,
PacketTooBig: 0u64,
Error: 0u64,
ErrorDeliveredToTransportLayer: 0u64,
ErrorDeliveredToSocket: 0u64,
"NDP": {
NeighborSolicitation: 0u64,
NeighborAdvertisement: 0u64,
RouterSolicitation: 0u64,
RouterAdvertisement: 0u64,
},
},
"Tx": {
Reply: 0u64,
AddressUnreachable: 0u64,
ProtocolUnreachable: 0u64,
PortUnreachable: 0u64,
NetUnreachable: 0u64,
TtlExpired: 0u64,
PacketTooBig: 0u64,
ParameterProblem: 0u64,
DestUnreachable: 0u64,
Error: 0u64,
"NDP": {
NeighborAdvertisement: 0u64,
NeighborSolicitation: 0u64,
},
},
},
},
"IPv4": {
PacketTx: 1u64,
"PacketRx": {
Received: 1u64,
Dispatched: 1u64,
Delivered: 1u64,
OtherHost: 0u64,
ParameterProblem: 0u64,
UnspecifiedDst: 0u64,
UnspecifiedSrc: 0u64,
Dropped: 0u64,
},
"Forwarding": {
Forwarded: 0u64,
ForwardingDisabled: 0u64,
NoRouteToHost: 0u64,
MtuExceeded: 0u64,
TtlExpired: 0u64,
},
RxIcmpError: 0u64,
"Fragments": {
ReassemblyError: 0u64,
NeedMoreFragments: 0u64,
InvalidFragment: 0u64,
CacheFull: 0u64,
},
},
"IPv6": {
PacketTx: 0u64,
"PacketRx": {
Received: 0u64,
Dispatched: 0u64,
DeliveredMulticast: 0u64,
DeliveredUnicast: 0u64,
OtherHost: 0u64,
ParameterProblem: 0u64,
UnspecifiedDst: 0u64,
UnspecifiedSrc: 0u64,
Dropped: 0u64,
DroppedTentativeDst: 0u64,
DroppedNonUnicastSrc: 0u64,
DroppedExtensionHeader: 0u64,
},
"Forwarding": {
Forwarded: 0u64,
ForwardingDisabled: 0u64,
NoRouteToHost: 0u64,
MtuExceeded: 0u64,
TtlExpired: 0u64,
},
RxIcmpError: 0u64,
"Fragments": {
ReassemblyError: 0u64,
NeedMoreFragments: 0u64,
InvalidFragment: 0u64,
CacheFull: 0u64,
},
},
"UDP": {
"V4": {
"Rx": {
Received: 1u64,
"Errors": {
MappedAddr: 0u64,
UnknownDstPort: 0u64,
Malformed: 0u64,
},
},
"Tx": {
Sent: 1u64,
Errors: 0u64,
},
IcmpErrors: 0u64,
},
"V6": {
"Rx": {
Received: 0u64,
"Errors": {
MappedAddr: 0u64,
UnknownDstPort: 0u64,
Malformed: 0u64,
},
},
"Tx": {
Sent: 0u64,
Errors: 0u64,
},
IcmpErrors: 0u64,
},
},
"TCP": {
"V4": {
PassiveConnectionOpenings: 0u64,
ActiveConnectionOpenings: 0u64,
FastRecovery: 0u64,
EstablishedClosed: 0u64,
EstablishedResets: 0u64,
EstablishedTimedout: 0u64,
"Rx": {
ValidSegmentsReceived: 0u64,
ReceivedSegmentsDispatched: 0u64,
ResetsReceived: 0u64,
SynsReceived: 0u64,
FinsReceived: 0u64,
"Errors": {
ChecksumErrors: 0u64,
InvalidIpAddrsReceived: 0u64,
InvalidSegmentsReceived: 0u64,
ReceivedSegmentsNoDispatch: 0u64,
ListenerQueueOverflow: 0u64,
PassiveOpenNoRouteErrors: 0u64,
},
},
"Tx": {
SegmentsSent: 0u64,
ResetsSent: 0u64,
SynsSent: 0u64,
FinsSent: 0u64,
Timeouts: 0u64,
Retransmits: 0u64,
FastRetransmits: 0u64,
SlowStartRetransmits: 0u64,
"Errors": {
SegmentSendErrors: 0u64,
ActiveOpenNoRouteErrors: 0u64,
}
},
"Errors": {
FailedConnectionOpenings: 0u64,
FailedPortReservations: 0u64,
}
},
"V6": {
PassiveConnectionOpenings: 0u64,
ActiveConnectionOpenings: 0u64,
FastRecovery: 0u64,
EstablishedClosed: 0u64,
EstablishedResets: 0u64,
EstablishedTimedout: 0u64,
"Rx": {
ValidSegmentsReceived: 0u64,
ReceivedSegmentsDispatched: 0u64,
ResetsReceived: 0u64,
SynsReceived: 0u64,
FinsReceived: 0u64,
"Errors": {
ChecksumErrors: 0u64,
InvalidIpAddrsReceived: 0u64,
InvalidSegmentsReceived: 0u64,
ReceivedSegmentsNoDispatch: 0u64,
ListenerQueueOverflow: 0u64,
PassiveOpenNoRouteErrors: 0u64,
},
},
"Tx": {
SegmentsSent: 0u64,
ResetsSent: 0u64,
SynsSent: 0u64,
FinsSent: 0u64,
Timeouts: 0u64,
Retransmits: 0u64,
FastRetransmits: 0u64,
SlowStartRetransmits: 0u64,
"Errors": {
SegmentSendErrors: 0u64,
ActiveOpenNoRouteErrors: 0u64,
}
},
"Errors": {
FailedConnectionOpenings: 0u64,
FailedPortReservations: 0u64,
}
},
},
}
})
}
#[netstack_test]
async fn inspect_filtering_state(name: &str) {
use fnet_filter_ext::{
Action, AddressMatcher, AddressMatcherType, Change, Controller, ControllerId, Domain,
InstalledIpRoutine, InterfaceMatcher, IpHook, Matchers, Namespace, NamespaceId,
PortMatcher, Resource, Routine, RoutineId, RoutineType, Rule, RuleId,
TransportProtocolMatcher,
};
let sandbox = netemul::TestSandbox::new().expect("create sandbox");
let realm = sandbox.create_netstack_realm::<Netstack3, _>(name).expect("create realm");
let control = realm
.connect_to_protocol::<fnet_filter::ControlMarker>()
.expect("connect to filter control");
let id = ControllerId(String::from("inspect"));
let mut controller = Controller::new(&control, &id).await.expect("open filter controller");
// By default, the netstack should report all the filtering hooks for both
// IP versions, but have no filtering state configured.
let data =
get_inspect_data(&realm, "netstack", "root", constants::inspect::DEFAULT_INSPECT_TREE_NAME)
.await
.expect("inspect data should be present");
// Debug print the tree to make debugging easier in case of failures.
println!("got inspect data: {:#?}", data);
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Filtering State": {
"IPv4": {
"ingress": {
"routines": 0u64,
},
"local_ingress": {
"routines": 0u64,
},
"forwarding": {
"routines": 0u64,
},
"local_egress": {
"routines": 0u64,
},
"egress": {
"routines": 0u64,
},
"uninstalled": {
"routines": 0u64,
},
},
"IPv6": {
"ingress": {
"routines": 0u64,
},
"local_ingress": {
"routines": 0u64,
},
"forwarding": {
"routines": 0u64,
},
"local_egress": {
"routines": 0u64,
},
"egress": {
"routines": 0u64,
},
"uninstalled": {
"routines": 0u64,
},
},
}
});
let namespace = NamespaceId(String::from("test-namespace"));
let ingress_routine = RoutineId { namespace: namespace.clone(), name: String::from("ingress") };
let egress_routine = RoutineId { namespace: namespace.clone(), name: String::from("egress") };
let target_routine_name = String::from("target");
controller
.push_changes(
[
Resource::Namespace(Namespace { id: namespace.clone(), domain: Domain::AllIp }),
Resource::Routine(Routine {
id: ingress_routine.clone(),
routine_type: RoutineType::Ip(Some(InstalledIpRoutine {
hook: IpHook::Ingress,
priority: -10,
})),
}),
Resource::Rule(Rule {
id: RuleId { routine: ingress_routine.clone(), index: 20 },
matchers: Matchers {
in_interface: Some(InterfaceMatcher::Id(NonZeroU64::new(1).unwrap())),
..Default::default()
},
action: Action::Drop,
}),
Resource::Rule(Rule {
id: RuleId { routine: ingress_routine, index: 10 },
matchers: Matchers {
transport_protocol: Some(TransportProtocolMatcher::Tcp {
src_port: None,
dst_port: Some(PortMatcher::new(22, 22, /* invert */ false).unwrap()),
}),
..Default::default()
},
action: Action::Drop,
}),
Resource::Routine(Routine {
id: RoutineId { namespace, name: target_routine_name.clone() },
routine_type: RoutineType::Ip(None),
}),
Resource::Routine(Routine {
id: egress_routine.clone(),
routine_type: RoutineType::Ip(Some(InstalledIpRoutine {
hook: IpHook::Egress,
priority: -10,
})),
}),
Resource::Rule(Rule {
id: RuleId { routine: egress_routine, index: 0 },
matchers: Matchers {
dst_addr: Some(AddressMatcher {
matcher: AddressMatcherType::Subnet(
fidl_subnet!("127.0.0.0/8").try_into().unwrap(),
),
invert: false,
}),
..Default::default()
},
action: Action::Jump(target_routine_name),
}),
]
.into_iter()
.map(Change::Create)
.collect(),
)
.await
.expect("push filter changes");
controller.commit().await.expect("commit filter changes");
let data =
get_inspect_data(&realm, "netstack", "root", constants::inspect::DEFAULT_INSPECT_TREE_NAME)
.await
.expect("inspect data should be present");
// Debug print the tree to make debugging easier in case of failures.
println!("got inspect data: {:#?}", data);
diagnostics_assertions::assert_data_tree!(data, "root": contains {
"Filtering State": {
"IPv4": {
"ingress": {
"routines": 1u64,
"0": {
"rules": 2u64,
"0": {
"matchers": {
"transport_protocol": "TransportProtocolMatcher { \
proto: TCP, \
src_port: None, \
dst_port: Some(PortMatcher { range: 22..=22, invert: false }) \
}",
},
"action": "Drop",
},
"1": {
"matchers": {
"in_interface": "Id(1)",
},
"action": "Drop",
},
},
},
"local_ingress": {
"routines": 0u64,
},
"forwarding": {
"routines": 0u64,
},
"local_egress": {
"routines": 0u64,
},
"egress": {
"routines": 1u64,
"0": {
"rules": 1u64,
"0": {
// Note that this rule is only included in the IPv4 filtering state
// because it has an address matcher with an IPv4 subnet.
"matchers": {
"dst_address": "AddressMatcher { \
matcher: Subnet(127.0.0.0/8), \
invert: false \
}",
},
"action": "Jump(UninstalledRoutine(2))",
},
},
},
// Because the uninstalled routine is only jumped to from an IPv4 routine, it
// only exists in the IPv4 filtering state.
"uninstalled": {
"routines": 1u64,
"2": {
"rules": 0u64,
},
},
},
"IPv6": {
"ingress": {
"routines": 1u64,
"0": {
"rules": 2u64,
"0": {
"matchers": {
"transport_protocol": "TransportProtocolMatcher { \
proto: TCP, \
src_port: None, \
dst_port: Some(PortMatcher { range: 22..=22, invert: false }) \
}",
},
"action": "Drop",
},
"1": {
"matchers": {
"in_interface": "Id(1)",
},
"action": "Drop",
},
},
},
"local_ingress": {
"routines": 0u64,
},
"forwarding": {
"routines": 0u64,
},
"local_egress": {
"routines": 0u64,
},
"egress": {
"routines": 1u64,
"0": {
"rules": 0u64,
},
},
"uninstalled": {
"routines": 0u64,
},
},
}
});
}