use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::sync_channel;
use std::sync::{Arc, Condvar, Mutex, OnceLock};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
pub const TCP_FIN: u8 = 0x01;
pub const TCP_SYN: u8 = 0x02;
pub const TCP_RST: u8 = 0x04;
pub const TCP_ACK: u8 = 0x10;
const TCP_HEADER_LEN: usize = 20;
const IPV4_HEADER_LEN: usize = 20;
const IPPROTO_TCP: u8 = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SynOutcome {
Open,
Closed,
Filtered,
}
pub fn classify_flags(flags: u8) -> SynOutcome {
if flags & TCP_RST != 0 {
SynOutcome::Closed
} else if flags & TCP_SYN != 0 && flags & TCP_ACK != 0 {
SynOutcome::Open
} else {
SynOutcome::Filtered
}
}
fn checksum(data: &[u8]) -> u16 {
let mut sum: u32 = 0;
let (pairs, remainder) = data.as_chunks::<2>();
for pair in pairs {
sum += u32::from(u16::from_be_bytes(*pair));
}
if let [last] = remainder {
sum += u32::from(u16::from_be_bytes([*last, 0]));
}
while sum >> 16 != 0 {
sum = (sum & 0xffff) + (sum >> 16);
}
!(sum as u16)
}
pub fn build_tcp_syn(
src: Ipv4Addr,
dst: Ipv4Addr,
src_port: u16,
dst_port: u16,
seq: u32,
) -> Vec<u8> {
let mut seg = vec![0u8; TCP_HEADER_LEN];
seg[0..2].copy_from_slice(&src_port.to_be_bytes());
seg[2..4].copy_from_slice(&dst_port.to_be_bytes());
seg[4..8].copy_from_slice(&seq.to_be_bytes());
seg[12] = 0x50; seg[13] = TCP_SYN;
seg[14..16].copy_from_slice(&1024u16.to_be_bytes());
let mut pseudo = Vec::with_capacity(12 + TCP_HEADER_LEN);
pseudo.extend_from_slice(&src.octets());
pseudo.extend_from_slice(&dst.octets());
pseudo.push(0);
pseudo.push(IPPROTO_TCP);
pseudo.extend_from_slice(&(TCP_HEADER_LEN as u16).to_be_bytes());
pseudo.extend_from_slice(&seg);
let sum = checksum(&pseudo);
seg[16..18].copy_from_slice(&sum.to_be_bytes());
seg
}
pub fn build_ipv4_syn(
src: Ipv4Addr,
dst: Ipv4Addr,
src_port: u16,
dst_port: u16,
seq: u32,
) -> Vec<u8> {
let tcp = build_tcp_syn(src, dst, src_port, dst_port, seq);
let total_len = (IPV4_HEADER_LEN + tcp.len()) as u16;
let mut ip = vec![0u8; IPV4_HEADER_LEN];
ip[0] = 0x45; ip[2..4].copy_from_slice(&total_len.to_be_bytes());
ip[6..8].copy_from_slice(&0x4000u16.to_be_bytes()); ip[8] = 64; ip[9] = IPPROTO_TCP;
ip[12..16].copy_from_slice(&src.octets());
ip[16..20].copy_from_slice(&dst.octets());
let sum = checksum(&ip);
ip[10..12].copy_from_slice(&sum.to_be_bytes());
ip.extend_from_slice(&tcp);
ip
}
pub fn parse_ipv4_tcp_addrs(packet: &[u8]) -> Option<(Ipv4Addr, Ipv4Addr, u16, u16, u8)> {
if packet.len() < IPV4_HEADER_LEN {
return None;
}
if packet[0] >> 4 != 4 {
return None;
}
let ihl = (packet[0] & 0x0f) as usize * 4;
if ihl < IPV4_HEADER_LEN || packet.len() < ihl {
return None;
}
if packet[9] != IPPROTO_TCP {
return None;
}
let src_ip = Ipv4Addr::new(packet[12], packet[13], packet[14], packet[15]);
let dst_ip = Ipv4Addr::new(packet[16], packet[17], packet[18], packet[19]);
let tcp = &packet[ihl..];
if tcp.len() < TCP_HEADER_LEN {
return None;
}
let src_port = u16::from_be_bytes([tcp[0], tcp[1]]);
let dst_port = u16::from_be_bytes([tcp[2], tcp[3]]);
let flags = tcp[13];
Some((src_ip, dst_ip, src_port, dst_port, flags))
}
pub fn parse_ipv4_tcp(packet: &[u8]) -> Option<(u16, u16, u8)> {
parse_ipv4_tcp_addrs(packet)
.map(|(_, _, src_port, dst_port, flags)| (src_port, dst_port, flags))
}
pub fn raw_socket_available() -> bool {
use socket2::{Domain, Protocol, Socket, Type};
Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(i32::from(IPPROTO_TCP))),
)
.is_ok()
}
fn local_ipv4_for(dst: Ipv4Addr) -> Option<Ipv4Addr> {
let socket = crate::iface::udp_connect(SocketAddr::new(dst.into(), 80)).ok()?;
match socket.local_addr().ok()?.ip() {
std::net::IpAddr::V4(v4) => Some(v4),
std::net::IpAddr::V6(_) => None,
}
}
const SRC_PORT_BASE: u16 = 40000;
fn src_port_for(dst_port: u16) -> u16 {
SRC_PORT_BASE.wrapping_add(dst_port)
}
pub fn syn_scan_port(dst: Ipv4Addr, dst_port: u16, timeout: Duration) -> Option<SynOutcome> {
use socket2::{Domain, Protocol, Socket, Type};
let receiver = receiver()?;
crate::rate::gate();
let dbg = debug_enabled();
let src = local_ipv4_for(dst)?;
let src_port = src_port_for(dst_port);
#[allow(unused_mut)]
let mut packet = build_ipv4_syn(src, dst, src_port, dst_port, 0x1234_5678);
#[cfg(target_os = "macos")]
{
let ip_len = u16::from_be_bytes([packet[2], packet[3]]);
packet[2..4].copy_from_slice(&ip_len.to_ne_bytes());
let ip_off = u16::from_be_bytes([packet[6], packet[7]]);
packet[6..8].copy_from_slice(&ip_off.to_ne_bytes());
}
const IPPROTO_RAW: i32 = 255;
let socket = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::from(IPPROTO_RAW))).ok()?;
socket.set_header_included_v4(true).ok()?;
let destination: socket2::SockAddr = SocketAddr::new(dst.into(), dst_port).into();
match socket.send_to(&packet, &destination) {
Ok(n) => {
if dbg {
eprintln!("[syn] sent {n}B SYN {src}:{src_port} -> {dst}:{dst_port}");
}
}
Err(e) => {
if dbg {
eprintln!("[syn] send_to {dst}:{dst_port} failed: {e}");
}
return None;
}
}
let started = Instant::now();
let outcome = receiver.wait_for(dst, dst_port, timeout);
if dbg {
eprintln!(
"[syn] wait {dst}:{dst_port} = {outcome:?} after {:?}",
started.elapsed()
);
}
Some(outcome)
}
const READ_TIMEOUT_MS: i32 = 200;
const SNAPLEN: i32 = 128;
struct ReceiverShared {
outcomes: Mutex<HashMap<(Ipv4Addr, u16), SynOutcome>>,
signal: Condvar,
stop: AtomicBool,
}
pub struct SynReceiver {
shared: Arc<ReceiverShared>,
reader: Option<JoinHandle<()>>,
}
impl SynReceiver {
fn start(target: Ipv4Addr) -> Option<SynReceiver> {
let src = local_ipv4_for(target)?;
let shared = Arc::new(ReceiverShared {
outcomes: Mutex::new(HashMap::new()),
signal: Condvar::new(),
stop: AtomicBool::new(false),
});
let reader_shared = Arc::clone(&shared);
let (ready_tx, ready_rx) = sync_channel::<bool>(1);
let reader = thread::spawn(move || match open_capture(src) {
Some((cap, offset)) => {
let _ = ready_tx.send(true);
reader_loop(cap, offset, &reader_shared);
}
None => {
let _ = ready_tx.send(false);
}
});
match ready_rx.recv() {
Ok(true) => Some(SynReceiver {
shared,
reader: Some(reader),
}),
_ => {
let _ = reader.join();
None
}
}
}
fn wait_for(&self, target: Ipv4Addr, port: u16, timeout: Duration) -> SynOutcome {
let key = (target, port);
let deadline = Instant::now() + timeout;
let mut map = self
.shared
.outcomes
.lock()
.expect("syn receiver mutex poisoned");
loop {
if let Some(&outcome) = map.get(&key) {
return outcome;
}
let now = Instant::now();
if now >= deadline {
return SynOutcome::Filtered;
}
let (guard, _timed_out) = self
.shared
.signal
.wait_timeout(map, deadline - now)
.expect("syn receiver condvar poisoned");
map = guard;
}
}
}
impl Drop for SynReceiver {
fn drop(&mut self) {
self.shared.stop.store(true, Ordering::SeqCst);
if let Some(handle) = self.reader.take() {
let _ = handle.join();
}
}
}
static RECEIVER: OnceLock<SynReceiver> = OnceLock::new();
pub fn init_receiver(target: Ipv4Addr) -> bool {
if RECEIVER.get().is_some() {
return true;
}
match SynReceiver::start(target) {
Some(receiver) => RECEIVER.set(receiver).is_ok(),
None => false,
}
}
fn receiver() -> Option<&'static SynReceiver> {
RECEIVER.get()
}
fn open_capture(src: Ipv4Addr) -> Option<(pcap::Capture<pcap::Active>, usize)> {
let device = device_for(src)?;
let dbg = debug_enabled();
if dbg {
let addrs: Vec<_> = device.addresses.iter().map(|a| a.addr).collect();
eprintln!("[syn] src={src} device={} addrs={addrs:?}", device.name);
}
let mut cap = pcap::Capture::from_device(device)
.ok()?
.immediate_mode(true)
.snaplen(SNAPLEN)
.timeout(READ_TIMEOUT_MS)
.open()
.ok()?;
cap.filter("tcp and (tcp[13] & 6) != 0", true).ok()?;
let link = cap.get_datalink();
let offset = datalink_offset(link);
if dbg {
eprintln!("[syn] datalink={link:?} offset={offset}");
}
Some((cap, offset))
}
fn debug_enabled() -> bool {
static DEBUG: OnceLock<bool> = OnceLock::new();
*DEBUG.get_or_init(|| std::env::var_os("ASPHYXIA_SYN_DEBUG").is_some())
}
fn device_for(src: Ipv4Addr) -> Option<pcap::Device> {
let list = pcap::Device::list().ok()?;
list.into_iter()
.find(|dev| {
dev.addresses
.iter()
.any(|a| matches!(a.addr, IpAddr::V4(v4) if v4 == src))
})
.or_else(|| pcap::Device::lookup().ok().flatten())
}
fn datalink_offset(link: pcap::Linktype) -> usize {
if link == pcap::Linktype::ETHERNET {
14
} else if link == pcap::Linktype::NULL || link == pcap::Linktype::LOOP {
4
} else if link == pcap::Linktype::LINUX_SLL {
16
} else if link == pcap::Linktype::RAW {
0
} else {
14
}
}
fn extract_ipv4_tcp(frame: &[u8], hint: usize) -> Option<(Ipv4Addr, u16, u16, u8)> {
for offset in [hint, 0, 14, 4, 16] {
if frame.len() > offset
&& frame[offset] >> 4 == 4
&& let Some((src_ip, _dst_ip, src_port, dst_port, flags)) =
parse_ipv4_tcp_addrs(&frame[offset..])
{
return Some((src_ip, src_port, dst_port, flags));
}
}
None
}
fn reader_loop(mut cap: pcap::Capture<pcap::Active>, offset: usize, shared: &ReceiverShared) {
let dbg = debug_enabled();
while !shared.stop.load(Ordering::Relaxed) {
match cap.next_packet() {
Ok(packet) => {
let Some((src_ip, src_port, dst_port, flags)) =
extract_ipv4_tcp(packet.data, offset)
else {
if dbg {
let n = packet.data.len().min(20);
eprintln!(
"[syn] unparsed frame len={} head={:02x?}",
packet.data.len(),
&packet.data[..n]
);
}
continue;
};
let outcome = classify_flags(flags);
if dbg {
eprintln!(
"[syn] pkt src={src_ip} sport={src_port} dport={dst_port} flags={flags:#04x} outcome={outcome:?}"
);
}
if outcome == SynOutcome::Filtered {
continue;
}
let mut map = shared.outcomes.lock().expect("syn receiver mutex poisoned");
map.entry((src_ip, src_port)).or_insert(outcome);
shared.signal.notify_all();
}
Err(pcap::Error::TimeoutExpired) => continue,
Err(_) => break,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classify_covers_open_closed_filtered() {
assert_eq!(classify_flags(TCP_SYN | TCP_ACK), SynOutcome::Open);
assert_eq!(classify_flags(TCP_RST | TCP_ACK), SynOutcome::Closed);
assert_eq!(classify_flags(TCP_RST), SynOutcome::Closed);
assert_eq!(classify_flags(TCP_ACK), SynOutcome::Filtered);
assert_eq!(classify_flags(0), SynOutcome::Filtered);
}
#[test]
fn checksum_of_a_valid_block_verifies_to_zero() {
let ip = build_ipv4_syn(
"10.0.0.1".parse().unwrap(),
"10.0.0.2".parse().unwrap(),
40000,
80,
0x11223344,
);
assert_eq!(checksum(&ip[..IPV4_HEADER_LEN]), 0);
}
#[test]
fn tcp_syn_has_expected_shape_and_valid_checksum() {
let src: Ipv4Addr = "192.168.1.10".parse().unwrap();
let dst: Ipv4Addr = "192.168.1.20".parse().unwrap();
let seg = build_tcp_syn(src, dst, 50000, 443, 0xdeadbeef);
assert_eq!(seg.len(), TCP_HEADER_LEN);
assert_eq!(u16::from_be_bytes([seg[0], seg[1]]), 50000);
assert_eq!(u16::from_be_bytes([seg[2], seg[3]]), 443);
assert_eq!(seg[12] >> 4, 5, "data offset should be 5 words");
assert_eq!(seg[13], TCP_SYN, "only the SYN flag should be set");
let mut pseudo = Vec::new();
pseudo.extend_from_slice(&src.octets());
pseudo.extend_from_slice(&dst.octets());
pseudo.push(0);
pseudo.push(IPPROTO_TCP);
pseudo.extend_from_slice(&(TCP_HEADER_LEN as u16).to_be_bytes());
pseudo.extend_from_slice(&seg);
assert_eq!(checksum(&pseudo), 0);
}
#[test]
fn ipv4_syn_total_length_and_protocol_are_correct() {
let ip = build_ipv4_syn(
"1.2.3.4".parse().unwrap(),
"5.6.7.8".parse().unwrap(),
33333,
22,
1,
);
assert_eq!(ip.len(), IPV4_HEADER_LEN + TCP_HEADER_LEN);
assert_eq!(ip[0] >> 4, 4, "version 4");
assert_eq!(
u16::from_be_bytes([ip[2], ip[3]]) as usize,
IPV4_HEADER_LEN + TCP_HEADER_LEN
);
assert_eq!(ip[9], IPPROTO_TCP);
}
#[test]
fn build_then_parse_round_trips_ports_and_flags() {
let ip = build_ipv4_syn(
"10.1.1.1".parse().unwrap(),
"10.1.1.2".parse().unwrap(),
44444,
8080,
42,
);
let (src_port, dst_port, flags) = parse_ipv4_tcp(&ip).expect("should parse");
assert_eq!(src_port, 44444);
assert_eq!(dst_port, 8080);
assert_eq!(flags, TCP_SYN);
}
#[test]
fn parse_rejects_non_ipv4_and_non_tcp() {
assert!(parse_ipv4_tcp(&[]).is_none());
assert!(parse_ipv4_tcp(&[0x60; 40]).is_none());
let mut pkt = build_ipv4_syn(
"1.1.1.1".parse().unwrap(),
"2.2.2.2".parse().unwrap(),
1,
2,
3,
);
pkt[9] = 17;
assert!(parse_ipv4_tcp(&pkt).is_none());
}
#[test]
fn parse_handles_ip_options() {
let mut pkt = build_ipv4_syn(
"9.9.9.9".parse().unwrap(),
"8.8.8.8".parse().unwrap(),
12345,
53,
7,
);
let tcp = pkt.split_off(IPV4_HEADER_LEN);
pkt.extend_from_slice(&[0u8; 4]); pkt.extend_from_slice(&tcp);
pkt[0] = 0x46; let new_len = pkt.len() as u16;
pkt[2..4].copy_from_slice(&new_len.to_be_bytes());
let (src_port, dst_port, flags) = parse_ipv4_tcp(&pkt).expect("should parse with options");
assert_eq!(src_port, 12345);
assert_eq!(dst_port, 53);
assert_eq!(flags, TCP_SYN);
}
#[test]
fn parse_addrs_extracts_endpoints() {
let src: Ipv4Addr = "203.0.113.7".parse().unwrap();
let dst: Ipv4Addr = "198.51.100.9".parse().unwrap();
let pkt = build_ipv4_syn(src, dst, 41443, 443, 1);
let (s_ip, d_ip, s_port, d_port, flags) =
parse_ipv4_tcp_addrs(&pkt).expect("should parse addrs");
assert_eq!(s_ip, src);
assert_eq!(d_ip, dst);
assert_eq!(s_port, 41443);
assert_eq!(d_port, 443);
assert_eq!(flags, TCP_SYN);
}
fn with_link_header(pkt: &[u8], len: usize) -> Vec<u8> {
let mut frame = vec![0xabu8; len];
frame.extend_from_slice(pkt);
frame
}
#[test]
fn extract_strips_ethernet_header_with_correct_hint() {
let src: Ipv4Addr = "10.0.0.5".parse().unwrap();
let pkt = build_ipv4_syn(src, "10.0.0.6".parse().unwrap(), 40080, 80, 9);
let frame = with_link_header(&pkt, 14);
let (s_ip, s_port, d_port, flags) = extract_ipv4_tcp(&frame, 14).expect("eth strip");
assert_eq!(s_ip, src);
assert_eq!(s_port, 40080);
assert_eq!(d_port, 80);
assert_eq!(flags, TCP_SYN);
}
#[test]
fn extract_recovers_when_the_hint_is_wrong() {
let pkt = build_ipv4_syn(
"172.16.0.1".parse().unwrap(),
"172.16.0.2".parse().unwrap(),
40022,
22,
3,
);
let frame = with_link_header(&pkt, 14);
let (_s_ip, s_port, d_port, _flags) =
extract_ipv4_tcp(&frame, 0).expect("should recover via fallback offsets");
assert_eq!(s_port, 40022);
assert_eq!(d_port, 22);
}
#[test]
fn extract_rejects_a_frame_without_ipv4() {
assert!(extract_ipv4_tcp(&[0xff; 40], 14).is_none());
}
#[test]
fn datalink_offsets_for_known_link_types() {
assert_eq!(datalink_offset(pcap::Linktype::ETHERNET), 14);
assert_eq!(datalink_offset(pcap::Linktype::NULL), 4);
assert_eq!(datalink_offset(pcap::Linktype::LOOP), 4);
assert_eq!(datalink_offset(pcap::Linktype::LINUX_SLL), 16);
assert_eq!(datalink_offset(pcap::Linktype::RAW), 0);
}
#[test]
fn source_ports_are_distinct_per_scanned_port() {
let a = src_port_for(22);
let b = src_port_for(443);
let c = src_port_for(6443);
assert_ne!(a, b);
assert_ne!(b, c);
assert_ne!(a, c);
}
#[test]
fn a_reply_identifies_its_port_by_source() {
let target: Ipv4Addr = "198.51.100.20".parse().unwrap();
let reply = build_ipv4_syn(target, "203.0.113.5".parse().unwrap(), 6443, 63839, 1);
let (src_ip, src_port, dst_port, _flags) =
extract_ipv4_tcp(&reply, 0).expect("parse reply");
assert_eq!(src_ip, target);
assert_eq!(src_port, 6443, "keying uses the reply's source port");
assert_ne!(
dst_port,
src_port_for(6443),
"the destination (our) port may be NAT-rewritten and is not used for keying"
);
}
}