net: raw packet tx capture

This commit is contained in:
Mark Poliakov 2025-02-10 15:14:14 +02:00
parent b8078561bf
commit 3f62374431
18 changed files with 343 additions and 72 deletions

View File

@ -65,7 +65,7 @@ pub fn handle(packet: L2Packet) {
let frame = packet.ethernet_frame();
let ty = EtherType::from_network_order(frame.ethertype);
RawSocket::packet_received(packet.clone());
RawSocket::handle_rx(packet.clone());
match ty {
EtherType::ARP => l3::arp::handle_packet(packet),

View File

@ -16,7 +16,10 @@ use yggdrasil_abi::{
net::{link::LinkState, protocols::EthernetFrame, MacAddress},
};
use crate::l3::{arp::ArpTable, Route};
use crate::{
l3::{arp::ArpTable, Route},
TxPacketBuilder,
};
pub trait NetworkDevice: Sync + Send {
fn allocate_transmit_buffer(&self, len: usize) -> Result<DmaBuffer<[MaybeUninit<u8>]>, Error>;
@ -109,18 +112,10 @@ impl NetworkInterface {
}
pub fn send_l2(&self, l2_frame: &EthernetFrame, l2_data: &[u8]) -> Result<(), Error> {
let l2_offset = self.device.packet_prefix_size();
let l2_data_offset = l2_offset + size_of::<EthernetFrame>();
let packet = self
.device
.allocate_transmit_buffer(l2_data_offset + l2_data.len())?;
let mut packet = unsafe { DmaBuffer::assume_init_slice(packet) };
packet[l2_offset..l2_data_offset].copy_from_slice(bytemuck::bytes_of(l2_frame));
packet[l2_data_offset..l2_data_offset + l2_data.len()].copy_from_slice(l2_data);
self.device.transmit_buffer(packet)
let mut builder = TxPacketBuilder::new(self, size_of::<EthernetFrame>() + l2_data.len())?;
builder.push_bytes(bytemuck::bytes_of(l2_frame))?;
builder.push_bytes(l2_data)?;
builder.transmit(None)
}
}

View File

@ -19,7 +19,7 @@ use yggdrasil_abi::{
},
};
use crate::{interface::NetworkInterface, l4, PacketBuilder};
use crate::{interface::NetworkInterface, l4, TxPacketBuilder};
pub mod arp;
pub mod ip;
@ -54,6 +54,7 @@ pub struct Route {
pub subnet: SubnetAddr,
pub interface: u32,
pub gateway: Option<IpAddr>,
pub priority: u32,
}
pub struct L4ResolvedPacket<'a, 'i> {
@ -114,12 +115,20 @@ impl Route {
}
let routes = ROUTES.read();
let mut best: Option<(&Route, IpAddr)> = None;
for route in routes.iter() {
if route.subnet.contains(&address) {
return Some((route.interface, route.gateway, address));
if route.subnet.contains(&address) || route.subnet.base() == address {
if let Some((prev_best, _)) = best.as_ref() {
if route.priority < prev_best.priority {
best = Some((route, address));
}
} else {
best = Some((route, address));
}
}
}
None
let (best, address) = best?;
Some((best.interface, best.gateway, address))
}
pub fn insert(route: Self) -> Result<(), Error> {
@ -220,8 +229,8 @@ pub async fn send_l4_ip_resolved(packet: &L4ResolvedPacket<'_, '_>) -> Result<()
let l3_frame = packet.make_l3_frame()?;
let mut builder = PacketBuilder::new(
packet.interface.device.as_ref(),
let mut builder = TxPacketBuilder::new(
packet.interface,
size_of::<EthernetFrame>() + size_of::<Ipv4Frame>() + packet.total_l4_len(),
)?;
builder.push(&EthernetFrame {
@ -233,9 +242,7 @@ pub async fn send_l4_ip_resolved(packet: &L4ResolvedPacket<'_, '_>) -> Result<()
builder.push_bytes(packet.l4_frame)?;
builder.push_bytes(packet.l4_options)?;
builder.push_bytes(packet.l4_data)?;
let (sent_packet, _len) = builder.finish();
packet.interface.device.transmit_buffer(sent_packet)
builder.transmit(None)
}
pub async fn send_l4_ip(packet: &L4UnresolvedPacket<'_>) -> Result<(), Error> {

View File

@ -8,10 +8,14 @@ use yggdrasil_abi::{
net::{
protocols::{IcmpV4Frame, InetChecksum, IpProtocol},
types::NetValueImpl,
SubnetAddr, SubnetV4Addr,
},
};
use crate::{l3, L3Packet};
use crate::{
l3::{self, Route},
L3Packet,
};
async fn send_v4_reply(
destination_ip: Ipv4Addr,
@ -64,6 +68,20 @@ async fn handle_v4(source_address: Ipv4Addr, l3_packet: L3Packet) -> Result<(),
match (icmp_frame.ty, icmp_frame.code) {
(8, 0) => send_v4_reply(source_address, icmp_frame, icmp_data).await,
// ICMP redirect to host
(5, 1) => {
// TODO don't just accept whatever gateway is supplied, it's a MitM asking to happen
let gateway = Ipv4Addr::from(u32::from_network_order(icmp_frame.rest));
log::warn!("ICMP redirect to host: {gateway}");
Route::insert(Route {
priority: 5,
interface: l3_packet.interface_id,
gateway: Some(IpAddr::V4(gateway)),
subnet: SubnetAddr::V4(SubnetV4Addr::from_address_mask(gateway, 32)),
})
.ok();
Ok(())
}
(0, 0) => Ok(()),
_ => {
log::debug!(

View File

@ -9,10 +9,11 @@ use core::mem::size_of;
use alloc::sync::Arc;
use bytemuck::Pod;
use ethernet::L2Packet;
use interface::NetworkDevice;
use interface::{NetworkDevice, NetworkInterface};
use l3::L3Packet;
use libk::{dma::DmaBuffer, task::runtime};
use libk_util::queue::UnboundedMpmcQueue;
use socket::RawSocket;
use yggdrasil_abi::{error::Error, net::protocols::EthernetFrame};
pub mod ethernet;
@ -26,28 +27,29 @@ pub mod util;
pub use interface::register_interface;
pub struct Packet {
// TODO info about "received" interface
pub struct RxPacket {
buffer: DmaBuffer<[u8]>,
offset: usize,
iface: u32,
}
pub struct PacketBuilder {
pub struct TxPacketBuilder<'a> {
nic: &'a NetworkInterface,
data: DmaBuffer<[u8]>,
pos: usize,
len: usize,
l2_offset: usize,
}
impl PacketBuilder {
pub fn new(nic: &dyn NetworkDevice, l2_size: usize) -> Result<Self, Error> {
let l2_offset = nic.packet_prefix_size();
let data = nic.allocate_transmit_buffer(l2_offset + l2_size)?;
impl<'a> TxPacketBuilder<'a> {
pub fn new(nic: &'a NetworkInterface, l2_size: usize) -> Result<Self, Error> {
let l2_offset = nic.device.packet_prefix_size();
let data = nic.device.allocate_transmit_buffer(l2_offset + l2_size)?;
let data = unsafe { DmaBuffer::assume_init_slice(data) };
Ok(Self {
nic,
data,
pos: l2_offset,
len: l2_offset,
l2_offset,
})
}
@ -65,12 +67,20 @@ impl PacketBuilder {
Ok(())
}
pub fn finish(self) -> (DmaBuffer<[u8]>, usize) {
(self.data, self.len)
fn finish(self) -> (DmaBuffer<[u8]>, usize) {
(self.data, self.pos)
}
pub fn transmit(self, mute_raw: Option<u32>) -> Result<(), Error> {
let nic = self.nic;
let (data, len) = self.finish();
// Also transmit to raw
RawSocket::handle_tx(nic.id, &data, len, mute_raw);
nic.device.transmit_buffer(data)
}
}
impl Packet {
impl RxPacket {
#[inline]
pub fn new(buffer: DmaBuffer<[u8]>, offset: usize, iface: u32) -> Self {
Self {
@ -81,11 +91,11 @@ impl Packet {
}
}
static PACKET_QUEUE: UnboundedMpmcQueue<Packet> = UnboundedMpmcQueue::new();
static PACKET_QUEUE: UnboundedMpmcQueue<RxPacket> = UnboundedMpmcQueue::new();
static ACCEPT_QUEUE: UnboundedMpmcQueue<L3Packet> = UnboundedMpmcQueue::new();
#[inline]
pub fn receive_packet(packet: Packet) -> Result<(), Error> {
pub fn receive_packet(packet: RxPacket) -> Result<(), Error> {
PACKET_QUEUE.push_back(packet);
Ok(())
}

View File

@ -276,6 +276,7 @@ fn add_route(
interface: interface.id,
gateway,
subnet,
priority: 10,
};
Route::insert(route).map_err(|_| "Could not insert route")?;
Ok(())

View File

@ -13,6 +13,7 @@ use libk::{
task::runtime::maybe_timeout,
vfs::{FileReadiness, Socket},
};
use libk_mm::PageBox;
use libk_util::{queue::BoundedMpmcQueue, sync::spin_rwlock::IrqSafeRwLock};
use yggdrasil_abi::{
net::{
@ -22,12 +23,17 @@ use yggdrasil_abi::{
option::OptionValue,
};
use crate::{ethernet::L2Packet, interface::NetworkInterface};
use crate::{ethernet::L2Packet, interface::NetworkInterface, TxPacketBuilder};
enum RawPacket {
Ingress(L2Packet),
Egress(Arc<PageBox<[u8]>>),
}
pub struct RawSocket {
id: u32,
bound: IrqSafeRwLock<Option<u32>>,
receive_queue: BoundedMpmcQueue<L2Packet>,
receive_queue: BoundedMpmcQueue<RawPacket>,
}
static RAW_SOCKET_ID: AtomicU32 = AtomicU32::new(0);
@ -48,31 +54,64 @@ impl RawSocket {
socket
}
fn bound_packet_received(&self, packet: L2Packet) {
fn bound_packet_received(&self, packet: RawPacket) {
// TODO do something with the dropped packet?
self.receive_queue.try_push_back(packet).ok();
}
pub fn packet_received(packet: L2Packet) {
pub fn handle_rx(packet: L2Packet) {
let bound_sockets = BOUND_RAW_SOCKETS.read();
let raw_sockets = RAW_SOCKETS.read();
if let Some(ids) = bound_sockets.get(&packet.interface_id) {
for id in ids {
let socket = raw_sockets.get(id).unwrap();
socket.bound_packet_received(packet.clone());
socket.bound_packet_received(RawPacket::Ingress(packet.clone()));
}
}
}
fn packet_to_user(packet: L2Packet, buffer: &mut [u8]) -> Result<usize, Error> {
let full_len = packet.data.len();
let len = full_len - packet.l2_offset;
if buffer.len() < len {
return Err(Error::BufferTooSmall);
pub fn handle_tx(iface: u32, packet: &DmaBuffer<[u8]>, len: usize, except: Option<u32>) {
let bound_sockets = BOUND_RAW_SOCKETS.read();
let raw_sockets = RAW_SOCKETS.read();
if let Some(ids) = bound_sockets.get(&iface) {
let Ok(egress) = PageBox::from_slice(&packet[..len]).map(Arc::new) else {
return;
};
for id in ids {
if except.map_or(false, |i| i == *id) {
continue;
}
let socket = raw_sockets.get(id).unwrap();
socket.bound_packet_received(RawPacket::Egress(egress.clone()));
}
}
}
fn packet_to_user(packet: RawPacket, buffer: &mut [u8]) -> Result<usize, Error> {
match packet {
RawPacket::Egress(egress) => {
// TODO l2offset in egress?
let len = egress.len();
if buffer.len() < len {
return Err(Error::BufferTooSmall);
}
buffer[..len].copy_from_slice(&egress[..]);
Ok(len)
}
RawPacket::Ingress(ingress) => {
let full_len = ingress.data.len();
let len = full_len - ingress.l2_offset;
if buffer.len() < len {
return Err(Error::BufferTooSmall);
}
buffer[..len].copy_from_slice(&ingress.data[ingress.l2_offset..full_len]);
Ok(len)
}
}
buffer[..len].copy_from_slice(&packet.data[packet.l2_offset..full_len]);
Ok(len)
}
}
@ -167,16 +206,14 @@ impl Socket for RawSocket {
// TODO cap by MTU?
let bound = self.bound.read().ok_or(Error::InvalidOperation)?;
let interface = NetworkInterface::get(bound)?;
let l2_offset = interface.device.packet_prefix_size();
if message.payload.len() > 4096 - l2_offset {
// let l2_offset = interface.device.packet_prefix_size();
if message.payload.len() > 1024 {
return Err(Error::InvalidArgument);
}
let packet = interface
.device
.allocate_transmit_buffer(l2_offset + message.payload.len())?;
let mut packet = unsafe { DmaBuffer::assume_init_slice(packet) };
packet[l2_offset..l2_offset + message.payload.len()].copy_from_slice(message.payload);
interface.device.transmit_buffer(packet)?;
let mut builder = TxPacketBuilder::new(&*interface, message.payload.len())?;
builder.push_bytes(message.payload)?;
// false to prevent loopback
builder.transmit(Some(self.id))?;
Ok(message.payload.len())
}
async fn send_message(

View File

@ -21,7 +21,7 @@ use regs::{Regs, ICR};
use tock_registers::interfaces::Writeable;
use ygg_driver_net_core::{
interface::{NetworkDevice, NetworkInterfaceType},
register_interface, Packet,
register_interface, RxPacket,
};
use ygg_driver_pci::{
device::{PciDeviceInfo, PreferredInterruptMode},
@ -322,7 +322,7 @@ impl InterruptHandler for Igbe {
let nic = *self.nic.get();
let head = regs.rx_queue_head();
let tail = rx.handle_rx(&*self.dma, head, |packet, _| {
let packet = Packet::new(packet, 0, nic);
let packet = RxPacket::new(packet, 0, nic);
ygg_driver_net_core::receive_packet(packet).ok();
});
regs.set_rx_queue_tail(tail);

View File

@ -13,7 +13,7 @@ use libk::dma::{DmaBuffer, DummyDmaAllocator};
use libk_util::OneTimeInit;
use ygg_driver_net_core::{
interface::{NetworkDevice, NetworkInterfaceType},
Packet,
RxPacket,
};
use yggdrasil_abi::{error::Error, net::MacAddress};
@ -27,7 +27,7 @@ impl NetworkDevice for LoopbackDevice {
}
fn transmit_buffer(&self, buffer: DmaBuffer<[u8]>) -> Result<(), Error> {
let packet = Packet::new(buffer, 0, *LOOPBACK_ID.get());
let packet = RxPacket::new(buffer, 0, *LOOPBACK_ID.get());
ygg_driver_net_core::receive_packet(packet)
}

View File

@ -16,7 +16,7 @@ use tock_registers::{
};
use ygg_driver_net_core::{
interface::{NetworkDevice, NetworkInterfaceType},
Packet,
RxPacket,
};
use ygg_driver_pci::device::PciDeviceInfo;
use yggdrasil_abi::net::MacAddress;
@ -325,7 +325,7 @@ impl InterruptHandler for Rtl8139 {
packet_buf.copy_from_slice(&rx.buffer[rx_pos + 4..rx_pos + rx_len + 4]);
let packet_buf = unsafe { DmaBuffer::assume_init_slice(packet_buf) };
// let packet_buf = unsafe { packet_buf.assume_init_slice() };
let packet = Packet::new(packet_buf, 0, nic);
let packet = RxPacket::new(packet_buf, 0, nic);
ygg_driver_net_core::receive_packet(packet).ok();
}
}

View File

@ -22,7 +22,7 @@ use tock_registers::{
};
use ygg_driver_net_core::{
interface::{NetworkDevice, NetworkInterfaceType},
Packet,
RxPacket,
};
use ygg_driver_pci::device::{PciDeviceInfo, PreferredInterruptMode};
use yggdrasil_abi::{
@ -579,7 +579,7 @@ impl InterruptHandler for Rtl8168 {
let count = rx
.consume(&*self.dma, |buffer| {
// TODO add packet len hint to packets
let packet = Packet::new(buffer, 0, nic);
let packet = RxPacket::new(buffer, 0, nic);
ygg_driver_net_core::receive_packet(packet).ok();
})
.unwrap_or(0);

View File

@ -19,7 +19,7 @@ use libk_util::{
};
use ygg_driver_net_core::{
interface::{NetworkDevice, NetworkInterfaceType},
Packet,
RxPacket,
};
use ygg_driver_pci::{
device::{PciDeviceInfo, PreferredInterruptMode},
@ -130,7 +130,7 @@ impl<T: Transport + 'static> VirtioNet<T> {
pending_packets.insert(token, buffer);
let packet = unsafe { DmaBuffer::assume_init_slice(packet) };
let packet = Packet::new(packet, size_of::<VirtioPacketHeader>(), interface_id);
let packet = RxPacket::new(packet, size_of::<VirtioPacketHeader>(), interface_id);
ygg_driver_net_core::receive_packet(packet).unwrap();
count += 1
}

View File

@ -4,6 +4,7 @@
step_trait,
const_trait_impl,
maybe_uninit_as_bytes,
maybe_uninit_write_slice,
negative_impls
)]
#![no_std]
@ -123,6 +124,15 @@ impl<T> PageBox<T, GlobalPhysicalAllocator> {
pub unsafe fn from_physical_raw(address: PhysicalAddress) -> PageBox<T> {
PageBox::from_physical_raw_in(address)
}
pub fn from_slice(slice: &[T]) -> Result<PageBox<[T]>, Error>
where
T: Copy,
{
let mut uninit = Self::new_uninit_slice(slice.len())?;
MaybeUninit::copy_from_slice(&mut uninit[..], slice);
Ok(unsafe { uninit.assume_init_slice() })
}
}
impl<T, A: PhysicalMemoryAllocator<Address = PhysicalAddress>> PageBox<T, A> {

View File

@ -247,9 +247,6 @@ impl AArch64 {
Cpu::init_local(None, per_cpu);
if is_bsp {
// Will register drivers
call_init_array();
atomic::compiler_fence(Ordering::SeqCst);
debug::init();
@ -258,6 +255,9 @@ impl AArch64 {
let bootargs = dt.chosen_bootargs().unwrap_or("");
config::parse_boot_arguments(bootargs);
// Will register drivers
call_init_array();
// Create device tree sysfs nodes
device_tree::util::create_sysfs_nodes(dt);

View File

@ -64,6 +64,11 @@ impl SubnetV4Addr {
(address & self.mask) == root
}
/// Returns the base address of the subnet
pub fn base(&self) -> Ipv4Addr {
self.root
}
fn make_root_mask(bits: u8) -> u32 {
!((1 << (32 - bits)) - 1)
}
@ -106,6 +111,13 @@ impl SubnetAddr {
(_, _) => false,
}
}
/// Returns the base address of this subnet
pub fn base(&self) -> IpAddr {
match self {
Self::V4(v4) => IpAddr::V4(v4.base()),
}
}
}
impl fmt::Display for SubnetAddr {

View File

@ -44,3 +44,7 @@ path = "src/dnsq.rs"
[[bin]]
name = "ping"
path = "src/ping.rs"
[[bin]]
name = "ncap"
path = "src/ncap.rs"

View File

@ -0,0 +1,176 @@
#![feature(yggdrasil_os, rustc_private)]
use std::{
fs::File,
io::{self, BufWriter, Write},
os::yggdrasil::{
io::net::raw_socket::RawSocket,
signal::{set_signal_handler, SignalHandler},
},
path::{Path, PathBuf},
process::ExitCode,
};
use bytemuck::{Pod, Zeroable};
use clap::Parser;
use ::yggdrasil_abi::net::protocols::EthernetFrame;
use runtime::abi as yggdrasil_abi;
use yggdrasil_abi::process::Signal;
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
struct PcapFileHeader {
magic: u32,
version_major: u16,
version_minor: u16,
_0: [u32; 2],
snap_len: u32,
link_type: u32,
}
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
#[repr(C)]
struct PcapRecordHeader {
ts_seconds: u32,
ts_nanoseconds: u32,
captured_len: u32,
original_len: u32,
}
#[derive(Parser, Debug)]
struct Args {
#[clap(
short,
help = "Write captured packets to a PCAP file (Default: write to stdout)"
)]
output: Option<PathBuf>,
#[clap(
short,
help = "When writing PCAP output to a file, also print packet information to stdout"
)]
noisy: bool,
#[clap(help = "Interface to capture from")]
interface: String,
}
struct PcapFile {
writer: BufWriter<File>,
header_written: bool,
}
impl PcapFile {
const MAGIC: u32 = 0xA1B2C3D4;
const VERSION_MAJOR: u16 = 2;
const VERSION_MINOR: u16 = 0;
fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
File::create(path)
.map(BufWriter::new)
.map(Self::from_writer)
}
fn from_writer(writer: BufWriter<File>) -> Self {
Self {
writer,
header_written: false,
}
}
fn write(&mut self, payload: &[u8]) -> io::Result<()> {
if !self.header_written {
// Write the file header
let header = PcapFileHeader {
magic: Self::MAGIC.to_be(),
version_minor: Self::VERSION_MINOR.to_be(),
version_major: Self::VERSION_MAJOR.to_be(),
_0: [0; 2],
snap_len: 4096u32.to_be(),
link_type: 0x1u32.to_be(),
};
self.writer.write_all(bytemuck::bytes_of(&header))?;
self.header_written = true;
}
let record = PcapRecordHeader {
ts_seconds: 0,
ts_nanoseconds: 0,
captured_len: (payload.len() as u32).to_be(),
original_len: (payload.len() as u32).to_be(),
};
self.writer.write_all(bytemuck::bytes_of(&record))?;
self.writer.write_all(payload)?;
Ok(())
}
}
enum Output {
File(PcapFile),
Stdout,
}
impl Output {
fn open(path: Option<&Path>) -> io::Result<Self> {
match path {
Some(path) => PcapFile::open(path).map(Self::File),
None => Ok(Self::Stdout),
}
}
fn write(&mut self, noisy: bool, payload: &[u8]) -> io::Result<()> {
match self {
Self::File(file) => {
if noisy {
describe_packet(payload);
}
file.write(payload)
}
Self::Stdout => {
describe_packet(payload);
Ok(())
}
}
}
}
fn describe_packet(payload: &[u8]) {
if payload.len() < size_of::<EthernetFrame>() {
eprintln!("Runt packet of {} bytes", payload.len());
return;
}
let ethernet: &EthernetFrame = bytemuck::from_bytes(&payload[..size_of::<EthernetFrame>()]);
println!(
"{} > {}, length {}",
ethernet.source_mac,
ethernet.destination_mac,
payload.len() - size_of::<EthernetFrame>()
);
}
fn run(args: &Args) -> io::Result<()> {
let mut output = Output::open(args.output.as_deref())?;
let socket = RawSocket::bind(args.interface.as_ref())?;
let mut buffer = [0; 4096];
// TODO packet timestamps
loop {
let len = socket.recv(&mut buffer)?;
output.write(args.noisy, &buffer[..len])?;
}
}
fn sigint_handler(_: Signal) {
debug_trace!("SIGINT handler!")
}
fn main() -> ExitCode {
let _ = set_signal_handler(Signal::Interrupted, SignalHandler::Function(sigint_handler));
let args = Args::parse();
match run(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}

View File

@ -54,6 +54,7 @@ const PROGRAMS: &[(&str, &str)] = &[
("http", "bin/http"),
("dnsq", "bin/dnsq"),
("ping", "bin/ping"),
("ncap", "sbin/ncap"),
// colors
("colors", "bin/colors"),
("term", "bin/term"),