flood-rs/src/raw_socket.rs

427 lines
15 KiB
Rust
Raw Normal View History

2026-07-18 03:05:55 +02:00
//! Raw AF_PACKET sender that constructs complete Ethernet + IP + UDP frames
//! in userspace and sends them directly to the NIC, bypassing the kernel's
//! UDP/IP stack entirely.
use std::fs;
use std::io::{self, BufRead, BufReader};
use std::net::{Ipv4Addr, ToSocketAddrs, UdpSocket};
use std::os::unix::io::AsRawFd;
/// Size of Ethernet + IPv4 + UDP headers combined.
const HEADER_SIZE: usize = 14 + 20 + 8; // 42 bytes
/// PACKET_QDISC_BYPASS socket option (skip the qdisc layer).
const PACKET_QDISC_BYPASS: libc::c_int = 20;
/// A raw AF_PACKET sender that constructs full Ethernet frames.
pub struct RawSender {
fd: libc::c_int,
ifindex: i32,
/// Pre-built header template (Ethernet + IP + UDP).
/// Only the IP total_length, IP checksum, and UDP length fields
/// need to be patched per packet.
header_template: [u8; HEADER_SIZE],
}
impl RawSender {
/// Creates a new RawSender.
///
/// Auto-discovers the network interface, MAC addresses, and source IP
/// from the routing table and ARP cache.
///
/// Requires `CAP_NET_RAW` or root.
pub fn new(dst_host: &str, dst_port: u16, src_port: u16) -> io::Result<Self> {
// Resolve destination IP.
let dst_ip: Ipv4Addr = dst_host
.parse()
.or_else(|_| {
(dst_host, dst_port)
.to_socket_addrs()?
.find_map(|a| match a {
std::net::SocketAddr::V4(v4) => Some(*v4.ip()),
_ => None,
})
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not resolve host"))
})?;
// Discover source IP and interface via a temporary connected UDP socket.
let probe = UdpSocket::bind("0.0.0.0:0")?;
probe.connect((dst_ip, dst_port))?;
let src_ip: Ipv4Addr = match probe.local_addr()? {
std::net::SocketAddr::V4(a) => *a.ip(),
_ => return Err(io::Error::new(io::ErrorKind::Other, "Not IPv4")),
};
// Discover interface name and index from routing.
let (ifname, ifindex) = discover_interface(dst_ip)?;
eprintln!("[raw] interface: {} (index {})", ifname, ifindex);
// Read source MAC from sysfs.
let src_mac = read_mac_from_sysfs(&ifname)?;
eprintln!("[raw] src MAC: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
src_mac[0], src_mac[1], src_mac[2], src_mac[3], src_mac[4], src_mac[5]);
// Look up destination MAC from ARP cache.
let dst_mac = lookup_arp(dst_ip, &ifname)?;
eprintln!("[raw] dst MAC: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
dst_mac[0], dst_mac[1], dst_mac[2], dst_mac[3], dst_mac[4], dst_mac[5]);
eprintln!("[raw] {}:{} -> {}:{}", src_ip, src_port, dst_ip, dst_port);
// Create AF_PACKET raw socket.
let fd = unsafe {
libc::socket(
libc::AF_PACKET,
libc::SOCK_RAW,
(libc::ETH_P_IP as u16).to_be() as i32,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
// Bind to the interface.
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
sll.sll_family = libc::AF_PACKET as u16;
sll.sll_protocol = (libc::ETH_P_IP as u16).to_be();
sll.sll_ifindex = ifindex;
let ret = unsafe {
libc::bind(
fd,
&sll as *const libc::sockaddr_ll as *const libc::sockaddr,
std::mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
)
};
if ret < 0 {
unsafe { libc::close(fd); }
return Err(io::Error::last_os_error());
}
// Skip qdisc for lower latency and higher throughput.
let bypass: libc::c_int = 1;
unsafe {
libc::setsockopt(
fd,
libc::SOL_PACKET,
PACKET_QDISC_BYPASS,
&bypass as *const libc::c_int as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
);
}
// Increase send buffer.
let sndbuf: libc::c_int = 25 * 1024 * 1024;
unsafe {
libc::setsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_SNDBUF,
&sndbuf as *const libc::c_int as *const libc::c_void,
std::mem::size_of::<libc::c_int>() as libc::socklen_t,
);
}
// Build the header template.
let header_template = build_header_template(
&src_mac, &dst_mac, src_ip, dst_ip, src_port, dst_port,
);
Ok(RawSender {
fd,
ifindex,
header_template,
})
}
/// Sends a batch of payloads as raw Ethernet frames using sendmmsg.
///
/// `frames` is a pre-allocated buffer of frame data.
/// Each frame is HEADER_SIZE + payload bytes.
/// Returns the number of frames successfully sent.
pub fn send_batch(&self, payloads: &[&[u8]], frame_buf: &mut Vec<u8>) -> io::Result<usize> {
let count = payloads.len();
if count == 0 {
return Ok(0);
}
// Build all frames into the contiguous buffer.
frame_buf.clear();
let mut offsets: Vec<(usize, usize)> = Vec::with_capacity(count);
for payload in payloads {
let frame_start = frame_buf.len();
let total_len = (20 + 8 + payload.len()) as u16; // IP total length
let udp_len = (8 + payload.len()) as u16;
// Copy header template.
frame_buf.extend_from_slice(&self.header_template);
// Patch IP total length (bytes 16-17 of the frame = offset 2-3 in IP header).
let ip_start = frame_start + 14;
frame_buf[ip_start + 2] = (total_len >> 8) as u8;
frame_buf[ip_start + 3] = total_len as u8;
// Patch IP header checksum (bytes 10-11 of IP header).
// Zero the checksum field first, then compute.
frame_buf[ip_start + 10] = 0;
frame_buf[ip_start + 11] = 0;
let cksum = ip_checksum(&frame_buf[ip_start..ip_start + 20]);
frame_buf[ip_start + 10] = (cksum >> 8) as u8;
frame_buf[ip_start + 11] = cksum as u8;
// Patch UDP length (bytes 4-5 of UDP header).
let udp_start = frame_start + 14 + 20;
frame_buf[udp_start + 4] = (udp_len >> 8) as u8;
frame_buf[udp_start + 5] = udp_len as u8;
// UDP checksum = 0 (optional for IPv4).
frame_buf[udp_start + 6] = 0;
frame_buf[udp_start + 7] = 0;
// Append payload.
frame_buf.extend_from_slice(payload);
let frame_end = frame_buf.len();
offsets.push((frame_start, frame_end));
}
// Build iovec and mmsghdr arrays for sendmmsg.
let mut iovecs: Vec<libc::iovec> = Vec::with_capacity(count);
let mut msghdrs: Vec<libc::mmsghdr> = Vec::with_capacity(count);
// Destination sockaddr_ll.
let mut sll: libc::sockaddr_ll = unsafe { std::mem::zeroed() };
sll.sll_family = libc::AF_PACKET as u16;
sll.sll_protocol = (libc::ETH_P_IP as u16).to_be();
sll.sll_ifindex = self.ifindex;
sll.sll_halen = 6;
// dst MAC is in the first 6 bytes of each frame, but sockaddr_ll
// also needs it. Copy from the template.
sll.sll_addr[..6].copy_from_slice(&self.header_template[..6]);
for &(start, end) in &offsets {
iovecs.push(libc::iovec {
iov_base: frame_buf[start..end].as_ptr() as *mut libc::c_void,
iov_len: end - start,
});
}
for iov in &iovecs {
let mut mhdr: libc::mmsghdr = unsafe { std::mem::zeroed() };
mhdr.msg_hdr.msg_iov = iov as *const libc::iovec as *mut libc::iovec;
mhdr.msg_hdr.msg_iovlen = 1;
mhdr.msg_hdr.msg_name = &sll as *const libc::sockaddr_ll as *mut libc::c_void;
mhdr.msg_hdr.msg_namelen = std::mem::size_of::<libc::sockaddr_ll>() as u32;
msghdrs.push(mhdr);
}
let ret = unsafe {
libc::sendmmsg(
self.fd,
msghdrs.as_mut_ptr(),
count as libc::c_uint,
0,
)
};
if ret < 0 {
Err(io::Error::last_os_error())
} else {
Ok(ret as usize)
}
}
/// Returns the header size so callers can compute total frame sizes.
pub fn header_size(&self) -> usize {
HEADER_SIZE
}
}
impl Drop for RawSender {
fn drop(&mut self) {
unsafe {
libc::close(self.fd);
}
}
}
/// Builds the 42-byte header template: Ethernet (14) + IP (20) + UDP (8).
///
/// Fields that vary per packet (IP total_length, IP checksum, UDP length)
/// are filled with placeholder values and must be patched before sending.
fn build_header_template(
src_mac: &[u8; 6],
dst_mac: &[u8; 6],
src_ip: Ipv4Addr,
dst_ip: Ipv4Addr,
src_port: u16,
dst_port: u16,
) -> [u8; HEADER_SIZE] {
let mut h = [0u8; HEADER_SIZE];
// -- Ethernet header (14 bytes) --
h[0..6].copy_from_slice(dst_mac);
h[6..12].copy_from_slice(src_mac);
h[12] = 0x08; // EtherType: IPv4
h[13] = 0x00;
// -- IPv4 header (20 bytes, no options) --
let ip = &mut h[14..34];
ip[0] = 0x45; // Version 4, IHL 5 (20 bytes)
ip[1] = 0x00; // DSCP/ECN
// ip[2..4] = total length (patched per packet)
ip[4] = 0x00; // Identification
ip[5] = 0x00;
ip[6] = 0x40; // Flags: Don't Fragment
ip[7] = 0x00; // Fragment offset
ip[8] = 64; // TTL
ip[9] = 17; // Protocol: UDP
// ip[10..12] = header checksum (patched per packet)
ip[12..16].copy_from_slice(&src_ip.octets());
ip[16..20].copy_from_slice(&dst_ip.octets());
// -- UDP header (8 bytes) --
let udp = &mut h[34..42];
udp[0] = (src_port >> 8) as u8;
udp[1] = src_port as u8;
udp[2] = (dst_port >> 8) as u8;
udp[3] = dst_port as u8;
// udp[4..6] = length (patched per packet)
// udp[6..8] = checksum (set to 0, optional for IPv4)
h
}
/// Computes the IPv4 header checksum (one's complement of the one's
/// complement sum of all 16-bit words in the header).
fn ip_checksum(header: &[u8]) -> u16 {
let mut sum: u32 = 0;
for i in (0..header.len()).step_by(2) {
let word = if i + 1 < header.len() {
((header[i] as u32) << 8) | (header[i + 1] as u32)
} else {
(header[i] as u32) << 8
};
sum += word;
}
// Fold carry bits.
while sum > 0xFFFF {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!(sum as u16)
}
/// Discovers the outbound interface name and index for a destination IP.
fn discover_interface(dst_ip: Ipv4Addr) -> io::Result<(String, i32)> {
// Use a temporary UDP socket to discover the interface.
let sock = UdpSocket::bind("0.0.0.0:0")?;
sock.connect((dst_ip, 80))?;
// Get the interface index via SO_BINDTODEVICE or by reading /proc/net/route.
// Simpler: use the socket's bound address to find the matching interface.
let local_ip = match sock.local_addr()? {
std::net::SocketAddr::V4(a) => *a.ip(),
_ => return Err(io::Error::new(io::ErrorKind::Other, "Not IPv4")),
};
// Scan /proc/net/if_inet6 or /proc/net/fib_trie... actually, simplest
// is to iterate /sys/class/net/*/address and check IPs.
// But even simpler: parse `ip route get` output... that's ugly.
//
// Let's iterate interfaces and match by IP.
let entries = fs::read_dir("/sys/class/net/")?;
for entry in entries {
let entry = entry?;
let ifname = entry.file_name().to_string_lossy().into_owned();
// Get the interface index.
let ifindex_path = format!("/sys/class/net/{}/ifindex", ifname);
let ifindex_str = match fs::read_to_string(&ifindex_path) {
Ok(s) => s,
Err(_) => continue,
};
let ifindex: i32 = match ifindex_str.trim().parse() {
Ok(i) => i,
Err(_) => continue,
};
// Check if this interface has the local IP using ioctl.
let fd = sock.as_raw_fd();
let mut ifr: libc::ifreq = unsafe { std::mem::zeroed() };
let name_bytes = ifname.as_bytes();
let copy_len = name_bytes.len().min(libc::IFNAMSIZ - 1);
unsafe {
std::ptr::copy_nonoverlapping(
name_bytes.as_ptr(),
ifr.ifr_name.as_mut_ptr() as *mut u8,
copy_len,
);
}
let ret = unsafe {
libc::ioctl(fd, libc::SIOCGIFADDR, &mut ifr)
};
if ret < 0 {
continue;
}
let addr = unsafe { &*(&ifr.ifr_ifru as *const _ as *const libc::sockaddr_in) };
let iface_ip = Ipv4Addr::from(u32::from_be(addr.sin_addr.s_addr));
if iface_ip == local_ip {
return Ok((ifname, ifindex));
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Could not find interface for IP {}", local_ip),
))
}
/// Reads the MAC address of a network interface from sysfs.
fn read_mac_from_sysfs(ifname: &str) -> io::Result<[u8; 6]> {
let path = format!("/sys/class/net/{}/address", ifname);
let mac_str = fs::read_to_string(path)?;
parse_mac(mac_str.trim())
}
/// Looks up the MAC address for an IP in the kernel ARP cache.
fn lookup_arp(ip: Ipv4Addr, ifname: &str) -> io::Result<[u8; 6]> {
let file = fs::File::open("/proc/net/arp")?;
let reader = BufReader::new(file);
let ip_str = ip.to_string();
for line in reader.lines().skip(1) {
let line = line?;
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() >= 6 && fields[0] == ip_str && fields[5] == ifname {
return parse_mac(fields[3]);
}
}
Err(io::Error::new(
io::ErrorKind::NotFound,
format!("No ARP entry for {} on {}", ip, ifname),
))
}
/// Parses a MAC address string like "50:6b:4b:c3:fa:9c" into 6 bytes.
fn parse_mac(s: &str) -> io::Result<[u8; 6]> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 6 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Invalid MAC: {}", s),
));
}
let mut mac = [0u8; 6];
for (i, part) in parts.iter().enumerate() {
mac[i] = u8::from_str_radix(part, 16).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, format!("Invalid MAC byte: {}", part))
})?;
}
Ok(mac)
}