diff --git a/Cargo.lock b/Cargo.lock index bae4f4c8f..67dc33745 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,12 +345,31 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -438,6 +457,12 @@ dependencies = [ "crc", ] +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "mbrman" version = "0.6.0" @@ -750,6 +775,25 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "smoltcp" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "heapless", + "managed", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "subtle" version = "2.6.1" @@ -950,6 +994,7 @@ version = "0.2.0" dependencies = [ "log", "qemu-exit", + "smoltcp", "uefi", "uefi-raw", ] diff --git a/uefi-test-runner/Cargo.toml b/uefi-test-runner/Cargo.toml index 3913de22a..fe534ac92 100644 --- a/uefi-test-runner/Cargo.toml +++ b/uefi-test-runner/Cargo.toml @@ -8,6 +8,7 @@ edition = "2024" [dependencies] uefi-raw = { path = "../uefi-raw" } uefi = { path = "../uefi", features = ["alloc", "global_allocator", "panic_handler", "logger", "qemu", "log-debugcon"] } +smoltcp = { version = "0.12.0", default-features = false, features = ["medium-ethernet", "proto-ipv4", "socket-udp"] } log.workspace = true diff --git a/uefi-test-runner/src/proto/network/mod.rs b/uefi-test-runner/src/proto/network/mod.rs index 0e21db626..16b5747e5 100644 --- a/uefi-test-runner/src/proto/network/mod.rs +++ b/uefi-test-runner/src/proto/network/mod.rs @@ -5,6 +5,9 @@ pub fn test() { http::test(); pxe::test(); + // Currently, we are in the unfortunate situation that the SNP test + // depends on the PXE test, as it assigns an IPv4 address to the + // interface via DHCP. snp::test(); } diff --git a/uefi-test-runner/src/proto/network/pxe.rs b/uefi-test-runner/src/proto/network/pxe.rs index 48d9c69f8..2c8b3fd1b 100644 --- a/uefi-test-runner/src/proto/network/pxe.rs +++ b/uefi-test-runner/src/proto/network/pxe.rs @@ -26,8 +26,11 @@ pub fn test() { assert!(base_code.mode().dhcp_ack_received()); let dhcp_ack: &DhcpV4Packet = base_code.mode().dhcp_ack().as_ref(); - let server_ip = dhcp_ack.bootp_si_addr; - let server_ip = IpAddress::new_v4(server_ip); + + info!("DHCP: Server IP: {:?}", dhcp_ack.bootp_si_addr); + info!("DHCP: Client IP: {:?}", dhcp_ack.bootp_yi_addr); + + let server_ip = IpAddress::new_v4(dhcp_ack.bootp_si_addr); const EXAMPLE_FILE_NAME: &[u8] = b"example-file.txt\0"; const EXAMPLE_FILE_CONTENT: &[u8] = b"Hello world!"; diff --git a/uefi-test-runner/src/proto/network/snp.rs b/uefi-test-runner/src/proto/network/snp.rs index 9ce416d9c..361cae891 100644 --- a/uefi-test-runner/src/proto/network/snp.rs +++ b/uefi-test-runner/src/proto/network/snp.rs @@ -1,141 +1,263 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 +use core::ops::DerefMut; use core::time::Duration; - +use smoltcp::wire::{ + ETHERNET_HEADER_LEN, EthernetFrame, IPV4_HEADER_LEN, Ipv4Packet, UDP_HEADER_LEN, UdpPacket, +}; +use uefi::boot::ScopedProtocol; use uefi::proto::network::MacAddress; use uefi::proto::network::snp::{InterruptStatus, ReceiveFlags, SimpleNetwork}; use uefi::{Status, boot}; +use uefi_raw::protocol::network::snp::NetworkState; -pub fn test() { - info!("Testing the simple network protocol"); +/// The MAC address configured for the interface. +const EXPECTED_MAC: [u8; 6] = [0x52, 0x54, 0, 0, 0, 0x1]; +const ETHERNET_PROTOCOL_IPV4: u16 = 0x0800; + +fn find_network_device() -> Option> { + let mut maybe_handle = None; let handles = boot::find_handles::().unwrap_or_default(); + // We iterate over all handles until we found the right network device. for handle in handles { - let simple_network = boot::open_protocol_exclusive::(handle); - if simple_network.is_err() { + let Ok(handle) = boot::open_protocol_exclusive::(handle) else { continue; - } - let simple_network = simple_network.unwrap(); - - // Check shutdown - let res = simple_network.shutdown(); - assert!(res == Ok(()) || res == Err(Status::NOT_STARTED.into())); - - // Check stop - let res = simple_network.stop(); - assert!(res == Ok(()) || res == Err(Status::NOT_STARTED.into())); - - // Check start - simple_network - .start() - .expect("Failed to start Simple Network"); - - // Check initialize - simple_network - .initialize(0, 0) - .expect("Failed to initialize Simple Network"); - - // edk2 virtio-net driver does not support statistics, so - // allow UNSUPPORTED (same for collect_statistics below). - let res = simple_network.reset_statistics(); - assert!(res == Ok(()) || res == Err(Status::UNSUPPORTED.into())); - - // Reading the interrupt status clears it - simple_network.get_interrupt_status().unwrap(); - - // Set receive filters - simple_network - .receive_filters( - ReceiveFlags::UNICAST | ReceiveFlags::BROADCAST, - ReceiveFlags::empty(), - false, - None, - ) - .expect("Failed to set receive filters"); - - // Check media - if !bool::from(simple_network.mode().media_present_supported) - || !bool::from(simple_network.mode().media_present) + }; + + // Check media is present + if !bool::from(handle.mode().media_present_supported) + || !bool::from(handle.mode().media_present) { continue; } - let payload = b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\ - \x45\x00\ - \x00\x21\ - \x00\x01\ - \x00\x00\ - \x10\ - \x11\ - \x07\x6a\ - \xc0\xa8\x11\x0f\ - \xc0\xa8\x11\x02\ - \x54\x45\ - \x54\x44\ - \x00\x0d\ - \xa9\xe4\ - \x04\x01\x02\x03\x04"; - - let dest_addr = MacAddress([0xffu8; 32]); - assert!( - !simple_network - .get_interrupt_status() - .unwrap() - .contains(InterruptStatus::TRANSMIT) - ); - - // Send the frame - simple_network - .transmit( - simple_network.mode().media_header_size as usize, - payload, - None, - Some(dest_addr), - Some(0x0800), - ) - .expect("Failed to transmit frame"); - - info!("Waiting for the transmit"); - while !simple_network + // Check MAC address + let has_mac = handle.mode().current_address.0[0..6] == EXPECTED_MAC + && handle.mode().permanent_address.0[0..6] == EXPECTED_MAC; + if !has_mac { + continue; + } + + maybe_handle.replace(handle); + } + + maybe_handle +} + +/// Receives the next IPv4 packet and prints corresponding metadata. +/// +/// Returns the length of the response. +fn receive(simple_network: &mut SimpleNetwork, buffer: &mut [u8]) -> uefi::Result { + // Wait for a bit to ensure that the previous packet has been processed. + boot::stall(Duration::from_millis(500)); + + let mut recv_src_mac = MacAddress([0; 32]); + let mut recv_dst_mac = MacAddress([0; 32]); + let mut recv_ethernet_protocol = 0; + + let res = simple_network.receive( + buffer, + None, + Some(&mut recv_src_mac), + Some(&mut recv_dst_mac), + Some(&mut recv_ethernet_protocol), + ); + + // To simplify debugging when receive an unexpected packet, we print the + // necessary info. This is especially useful if an unexpected IPv4 or ARP + // packet is received, which can easily happen when fiddling around with + // this test. + res.inspect(|_| { + debug!("Received:"); + debug!(" src_mac = {:x?}", &recv_src_mac.0[0..6]); + debug!(" dst_mac = {:x?}", &recv_dst_mac.0[0..6]); + debug!(" ethernet_proto=0x{:x?}", recv_ethernet_protocol); + + // Assert the ethernet frame was sent to the expected interface. + { + // UEFI reports proper DST MAC + assert_eq!(recv_dst_mac.0[0..6], EXPECTED_MAC); + + // Ethernet frame header reports proper DST MAC + let recv_frame = smoltcp::wire::EthernetFrame::new_checked(&buffer).unwrap(); + assert_eq!( + recv_frame.dst_addr(), + smoltcp::wire::EthernetAddress::from_bytes(&EXPECTED_MAC) + ); + } + + // Ensure that we do not accidentally get an ARP packet, which we + // do not expect in this test. + assert_eq!(recv_ethernet_protocol, ETHERNET_PROTOCOL_IPV4) + }) +} + +/// This test sends a simple UDP/IP packet to the `EchoService` (created by +/// `cargo xtask run`) and receives its response. +pub fn test() { + // This test currently depends on the PXE test running first. + if cfg!(not(feature = "pxe")) { + return; + } + + info!("Testing the simple network protocol"); + + // The handle to our specific network device, as the test requires also a + // specific environment. We do not test all possible handles. + let mut simple_network = find_network_device().unwrap_or_else(|| panic!( + "Failed to find SNP handle for network device with MAC address {:x}:{:x}:{:x}:{:x}:{:x}:{:x}", + EXPECTED_MAC[0], + EXPECTED_MAC[1], + EXPECTED_MAC[2], + EXPECTED_MAC[3], + EXPECTED_MAC[4], + EXPECTED_MAC[5] + )); + + assert_eq!( + simple_network.mode().state, + NetworkState::STOPPED, + "Should be in stopped state" + ); + + simple_network + .start() + .expect("Failed to start Simple Network"); + + simple_network + .initialize(0, 0) + .expect("Failed to initialize Simple Network"); + + // edk2 virtio-net driver does not support statistics, so + // allow UNSUPPORTED (same for collect_statistics below). + let res = simple_network.reset_statistics(); + assert!(res == Ok(()) || res == Err(Status::UNSUPPORTED.into())); + + // Reading the interrupt status clears it + simple_network.get_interrupt_status().unwrap(); + + // Set receive filters + simple_network + .receive_filters( + ReceiveFlags::UNICAST | ReceiveFlags::BROADCAST, + ReceiveFlags::empty(), + false, + None, + ) + .expect("Failed to set receive filters"); + + // High-level payload to send to destination + let payload = [ + 4_u8, /* Number of elements for echo service */ + 1, 2, 3, 4, + ]; + let frame = { + // IP that was obtained by PXE test running earlier + // TODO we should make these tests not depend on each other. + let src_ip = smoltcp::wire::Ipv4Address::new(192, 168, 17, 15); + let dst_ip = smoltcp::wire::Ipv4Address::new(192, 168, 17, 2); + + let udp_packet_len = UDP_HEADER_LEN + payload.len(); + let ipv4_packet_len = IPV4_HEADER_LEN + udp_packet_len; + let frame_len = ETHERNET_HEADER_LEN + ipv4_packet_len; + + let mut buffer = vec![0u8; frame_len]; + + let mut frame = EthernetFrame::new_unchecked(buffer.as_mut_slice()); + // Ethertype, SRC MAC, and DST MAC will be set by SNP's transmit(). + + let ipv4_packet_buffer = &mut frame.payload_mut()[0..ipv4_packet_len]; + let mut ipv4_packet = Ipv4Packet::new_unchecked(ipv4_packet_buffer); + ipv4_packet.set_header_len(IPV4_HEADER_LEN as u8 /* no extensions */); + ipv4_packet.set_total_len(ipv4_packet_len as u16); + ipv4_packet.set_hop_limit(16); + ipv4_packet.set_next_header(smoltcp::wire::IpProtocol::Udp); + ipv4_packet.set_dont_frag(true); + ipv4_packet.set_ident(0x1337); + ipv4_packet.set_version(4); + ipv4_packet.set_src_addr(src_ip); + ipv4_packet.set_dst_addr(dst_ip); + + let mut udp_packet = UdpPacket::new_unchecked(ipv4_packet.payload_mut()); + udp_packet.set_len(udp_packet_len as u16); + udp_packet.set_src_port(21573); + udp_packet.set_dst_port(21572); + udp_packet.payload_mut().copy_from_slice(&payload); + assert!(udp_packet.check_len().is_ok()); + + udp_packet.fill_checksum(&src_ip.into(), &dst_ip.into()); + // Do this last, as it depends on the other checksum. + ipv4_packet.fill_checksum(); + assert!(ipv4_packet.check_len().is_ok()); + + buffer + }; + + assert!( + !simple_network .get_interrupt_status() .unwrap() .contains(InterruptStatus::TRANSMIT) - {} + ); - // Attempt to receive a frame - let mut buffer = [0u8; 1500]; + // Send the frame + simple_network + .transmit( + simple_network.mode().media_header_size as usize, + &frame, + None, + Some(simple_network.mode().broadcast_address), + Some(ETHERNET_PROTOCOL_IPV4), + ) + .expect("Failed to transmit frame"); - info!("Waiting for the reception"); - if simple_network.receive(&mut buffer, None, None, None, None) - == Err(Status::NOT_READY.into()) - { - boot::stall(Duration::from_secs(1)); + info!("Waiting for the transmit"); + while !simple_network + .get_interrupt_status() + .unwrap() + .contains(InterruptStatus::TRANSMIT) + {} - simple_network - .receive(&mut buffer, None, None, None, None) - .unwrap(); - } + // Attempt to receive a frame + let mut buffer = [0u8; 1500]; - assert_eq!(buffer[42..47], [4, 4, 3, 2, 1]); + info!("Waiting for the reception"); + let n = receive(simple_network.deref_mut(), &mut buffer).unwrap(); + debug!("Reply has {n} bytes"); - // Get stats - let res = simple_network.collect_statistics(); - match res { - Ok(stats) => { - info!("Stats: {:?}", stats); + // Check payload in UDP packet that was reversed by our EchoService. + { + let recv_frame = EthernetFrame::new_checked(&buffer).unwrap(); + let recv_ipv4 = Ipv4Packet::new_checked(recv_frame.payload()).unwrap(); + let udp_packet = UdpPacket::new_checked(recv_ipv4.payload()).unwrap(); + assert_eq!(udp_packet.payload(), &[4, 4, 3, 2, 1]); + } - // One frame should have been transmitted and one received - assert_eq!(stats.tx_total_frames().unwrap(), 1); - assert_eq!(stats.rx_total_frames().unwrap(), 1); - } - Err(e) => { - if e == Status::UNSUPPORTED.into() { - info!("Stats: unsupported."); - } else { - panic!("{e}"); - } + // Get stats + let res = simple_network.collect_statistics(); + match res { + Ok(stats) => { + info!("Stats: {:?}", stats); + + // One frame should have been transmitted and one received + assert_eq!(stats.tx_total_frames().unwrap(), 1); + assert_eq!(stats.rx_total_frames().unwrap(), 1); + } + Err(e) => { + if e == Status::UNSUPPORTED.into() { + info!("Stats: unsupported."); + } else { + panic!("{e}"); } } } + + // Workaround for OVMF firmware. `stop()` works in CI on x86_64, but not + // x86 or aarch64. + if simple_network.mode().state == NetworkState::STARTED { + simple_network.stop().unwrap(); + } + simple_network.shutdown().unwrap(); } diff --git a/xtask/src/qemu.rs b/xtask/src/qemu.rs index 20cc7094d..b8aed7f05 100644 --- a/xtask/src/qemu.rs +++ b/xtask/src/qemu.rs @@ -498,7 +498,8 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> { "-netdev", "user,id=net0,net=192.168.17.0/24,tftp=uefi-test-runner/tftp/,bootfile=fake-boot-file", "-device", - "virtio-net-pci,netdev=net0", + // Some integration tests depend on this specific MAC. + "virtio-net-pci,netdev=net0,mac=52:54:00:00:00:01", ]); Some(net::EchoService::start()) } else {