wip
This commit is contained in:
parent
73c3462543
commit
f70c6c716a
10 changed files with 1006 additions and 191 deletions
426
src/raw_socket.rs
Normal file
426
src/raw_socket.rs
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
//! 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue