about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.rustfmt.toml2
-rw-r--r--Cargo.lock146
-rw-r--r--Cargo.toml8
-rw-r--r--src/ethertype.rs20
-rw-r--r--src/ippacket.rs99
-rw-r--r--src/layer3.rs65
-rw-r--r--src/main.rs186
8 files changed, 527 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/.rustfmt.toml b/.rustfmt.toml
new file mode 100644
index 0000000..4639247
--- /dev/null
+++ b/.rustfmt.toml
@@ -0,0 +1,2 @@
+hard_tabs = true
+chain_width = 100
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..41586eb
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,146 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "headlice"
+version = "0.1.0"
+dependencies = [
+ "pnet_datalink",
+ "scurvy",
+]
+
+[[package]]
+name = "ipnetwork"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "no-std-net"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
+
+[[package]]
+name = "pnet_base"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7"
+dependencies = [
+ "no-std-net",
+]
+
+[[package]]
+name = "pnet_datalink"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7"
+dependencies = [
+ "ipnetwork",
+ "libc",
+ "pnet_base",
+ "pnet_sys",
+ "winapi",
+]
+
+[[package]]
+name = "pnet_sys"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "scurvy"
+version = "0.1.0"
+source = "git+https://git.dreamy.place/scurvy/#c97099648a2117d44b3bc8bef033afde66c2339a"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..51b8fe6
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "headlice"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+pnet_datalink = "0.35.0"
+scurvy = { git = "https://git.dreamy.place/scurvy/" }
diff --git a/src/ethertype.rs b/src/ethertype.rs
new file mode 100644
index 0000000..0d728f3
--- /dev/null
+++ b/src/ethertype.rs
@@ -0,0 +1,20 @@
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum EtherType {
+	/// The frame type is IEEE 802.3 and this is it's length
+	Length(u16),
+	IPv4,
+	IPv6,
+	Unknown(u16),
+}
+
+impl EtherType {
+	//TODO: check ethertype is correct
+	pub fn new(n: u16) -> Self {
+		match n {
+			n if n <= 1500 => Self::Length(n),
+			0x0800 => Self::IPv4,
+			0x86DD => Self::IPv6,
+			n => Self::Unknown(n),
+		}
+	}
+}
diff --git a/src/ippacket.rs b/src/ippacket.rs
new file mode 100644
index 0000000..60858d2
--- /dev/null
+++ b/src/ippacket.rs
@@ -0,0 +1,99 @@
+use std::net::Ipv4Addr;
+
+pub struct Ipv4Packet<'p> {
+	/// The computed size of the IP packet
+	header_length: usize,
+	/// The total length of the IP packet when all fragments are combined
+	total_length: u16,
+	more_fragments: bool,
+	fragment_offset: usize,
+	next_header: IpNextHeader,
+	pub src: Ipv4Addr,
+	pub dst: Ipv4Addr,
+	payload: &'p [u8],
+}
+
+impl<'p> Ipv4Packet<'p> {
+	pub fn new(buffer: &'p [u8]) -> Self {
+		let ihl = buffer[0].to_be() & 0b0000_1111;
+		let header_length = ihl as usize * 4;
+
+		let total_length = u16::from_be_bytes([buffer[2], buffer[3]]);
+
+		// Fragmentation
+		let more_fragments = (buffer[6] & 0b0010_0000) > 0;
+		let fragment_offset = u16::from_be_bytes([buffer[6] & 0b0001_1111, buffer[7]]);
+		// Fragments are in units of 8 bytes
+		let true_frag_offset = fragment_offset as usize * 8;
+
+		let next_header = IpNextHeader::new(buffer[9]);
+
+		let src = Ipv4Addr::new(buffer[12], buffer[13], buffer[14], buffer[15]);
+		let dst = Ipv4Addr::new(buffer[16], buffer[17], buffer[18], buffer[19]);
+
+		Self {
+			header_length,
+			total_length,
+			more_fragments,
+			fragment_offset: true_frag_offset,
+			next_header,
+			src,
+			dst,
+			payload: &buffer[header_length..],
+		}
+	}
+
+	pub fn get_source(&self) -> Ipv4Addr {
+		self.src
+	}
+
+	pub fn get_destination(&self) -> Ipv4Addr {
+		self.dst
+	}
+
+	pub fn has_more_fragments(&self) -> bool {
+		self.more_fragments
+	}
+
+	pub fn get_fragment_offset(&self) -> usize {
+		self.fragment_offset
+	}
+
+	pub fn get_next_header(&self) -> IpNextHeader {
+		self.next_header
+	}
+
+	pub fn get_payload(&self) -> &[u8] {
+		&self.payload
+	}
+
+	pub fn get_packet_len(&self) -> usize {
+		self.total_length as usize
+	}
+
+	pub fn get_payload_len(&self) -> usize {
+		// An IPv4 header is always 20 bytes long
+		self.total_length as usize - 20
+	}
+}
+
+#[derive(Copy, Clone, Debug, PartialEq)]
+pub enum IpNextHeader {
+	Icmp,
+	Igmp,
+	Tcp,
+	Udp,
+	Unknown(u8),
+}
+
+impl IpNextHeader {
+	pub fn new(n: u8) -> Self {
+		match n {
+			1 => Self::Icmp,
+			2 => Self::Igmp,
+			6 => Self::Tcp,
+			17 => Self::Udp,
+			n => Self::Unknown(n),
+		}
+	}
+}
diff --git a/src/layer3.rs b/src/layer3.rs
new file mode 100644
index 0000000..7521c0d
--- /dev/null
+++ b/src/layer3.rs
@@ -0,0 +1,65 @@
+pub struct Tcp<'p> {
+	src: u16,
+	dst: u16,
+	payload: &'p [u8],
+}
+
+impl<'p> Tcp<'p> {
+	pub fn new(buffer: &'p [u8]) -> Self {
+		let src = u16::from_be_bytes([buffer[0], buffer[1]]);
+		let dst = u16::from_be_bytes([buffer[2], buffer[3]]);
+		let data_offset = (buffer[12].to_be() & 0b1111_0000) >> 4;
+		// Offset is number of 32-bit words
+		let true_offset = data_offset as usize * 4;
+
+		Self {
+			src,
+			dst,
+			payload: &buffer[true_offset..],
+		}
+	}
+
+	pub fn source_port(&self) -> u16 {
+		self.src
+	}
+
+	pub fn destination_port(&self) -> u16 {
+		self.dst
+	}
+
+	pub fn payload(&self) -> &[u8] {
+		self.payload
+	}
+}
+
+pub struct Udp<'p> {
+	src: u16,
+	dst: u16,
+	payload: &'p [u8],
+}
+
+impl<'p> Udp<'p> {
+	pub fn new(buffer: &'p [u8]) -> Self {
+		let src = u16::from_be_bytes([buffer[0], buffer[1]]);
+		let dst = u16::from_be_bytes([buffer[2], buffer[3]]);
+		let _len = u16::from_be_bytes([buffer[4], buffer[5]]);
+
+		Self {
+			src,
+			dst,
+			payload: &buffer[8..],
+		}
+	}
+
+	pub fn source_port(&self) -> u16 {
+		self.src
+	}
+
+	pub fn destination_port(&self) -> u16 {
+		self.dst
+	}
+
+	pub fn payload(&self) -> &[u8] {
+		self.payload
+	}
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..defd70b
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,186 @@
+use std::{
+	error::Error,
+	net::{Ipv4Addr, Shutdown},
+	sync::mpsc::{self, Receiver, Sender, channel},
+	thread::JoinHandle,
+	time::{Duration, Instant},
+};
+
+use ethertype::EtherType;
+use ippacket::{IpNextHeader, Ipv4Packet};
+use layer3::{Tcp, Udp};
+use pnet_datalink::{Channel, Config};
+use scurvy::{Argument, Scurvy};
+
+mod ethertype;
+mod ippacket;
+mod layer3;
+
+fn main() -> Result<(), Box<dyn Error>> {
+	let args = vec![
+		Argument::new(&["interface", "iface"]).arg("dev"),
+		Argument::new("ip").arg("addr"),
+	];
+	let scurvy = Scurvy::make(args);
+
+	let interface_name = scurvy.get("interface").unwrap();
+	let interface = match pnet_datalink::interfaces().iter().find(|i| i.name == interface_name) {
+		None => {
+			eprintln!("No interface found named '{interface_name}'");
+			return Ok(());
+		}
+		Some(i) => i.clone(),
+	};
+
+	let ip_want: Ipv4Addr = scurvy.get("ip").unwrap().parse()?;
+
+	let mut channel = match pnet_datalink::channel(&interface, Config::default())? {
+		Channel::Ethernet(_tx, rx) => rx,
+		_ => unimplemented!(),
+	};
+
+	let stat = stat_thread();
+
+	let tx_clone = stat.tx.clone();
+	std::thread::spawn(move || {
+		std::thread::sleep(Duration::from_secs(30));
+		tx_clone.send(Meow::Show).unwrap();
+	});
+
+	loop {
+		let pkt = channel.next()?;
+
+		let ethertype = EtherType::new(u16::from_be_bytes([pkt[12], pkt[13]]));
+		let eth_payload = &pkt[14..];
+
+		match ethertype {
+			EtherType::IPv4 => {
+				let ip = Ipv4Packet::new(eth_payload);
+				let ip_payload = ip.get_payload();
+
+				// 6 byte per MAC (x2), 2 byte ethertype, 2 byte crc
+				let total_l2_len = ip.get_packet_len() + 18;
+
+				match ip.get_next_header() {
+					IpNextHeader::Tcp => {
+						let tcp = Tcp::new(ip_payload);
+
+						let tcp_tx = if ip.src == ip_want {
+							true
+						} else if ip.dst == ip_want {
+							false
+						} else {
+							continue;
+						};
+
+						stat.tx.send(Meow::Tcp {
+							tx: tcp_tx,
+							src: tcp.source_port(),
+							dst: tcp.destination_port(),
+							len: total_l2_len,
+						})?;
+					}
+					IpNextHeader::Udp => {
+						let udp = Udp::new(ip_payload);
+
+						let udp_tx = if ip.src == ip_want {
+							true
+						} else if ip.dst == ip_want {
+							false
+						} else {
+							continue;
+						};
+
+						stat.tx.send(Meow::Udp {
+							tx: udp_tx,
+							src: udp.source_port(),
+							dst: udp.destination_port(),
+							len: total_l2_len,
+						})?;
+					}
+					_ => (),
+				}
+			}
+			_ => (),
+		}
+	}
+}
+
+struct StatHandle {
+	hwnd: JoinHandle<()>,
+	tx: Sender<Meow>,
+}
+
+enum Meow {
+	Tcp {
+		tx: bool,
+		src: u16,
+		dst: u16,
+		len: usize,
+	},
+	Udp {
+		tx: bool,
+		src: u16,
+		dst: u16,
+		len: usize,
+	},
+	Show,
+	Shutdown,
+}
+
+fn stat_thread() -> StatHandle {
+	let (tx, rx) = channel();
+
+	let hwnd = std::thread::spawn(|| stat(rx));
+
+	StatHandle { hwnd, tx }
+}
+
+fn stat(rx: Receiver<Meow>) {
+	let mut tcp_tx = vec![0; u16::MAX as usize];
+	let mut tcp_rx = vec![0; u16::MAX as usize];
+
+	let mut last_print = Instant::now();
+	loop {
+		if last_print.elapsed() > Duration::from_secs(10) {
+			let http_s = tcp_rx[80] + tcp_rx[443];
+			let http_s_kb = http_s / 1000;
+
+			let http_s_tx = tcp_tx[80] + tcp_tx[443];
+			let http_s_tx_kb = http_s_tx / 1000;
+
+			println!("HTTP(S) rx {}kB // tx {}kB", http_s_kb, http_s_tx_kb);
+
+			last_print = Instant::now();
+		}
+
+		match rx.recv() {
+			Err(_e) => {
+				eprintln!("error receiving! breaking from loop");
+				break;
+			}
+			Ok(Meow::Show) => {
+				if last_print.elapsed() > Duration::from_secs(30) {
+					println!("Activated by Meow::Show");
+				}
+			}
+			Ok(Meow::Shutdown) => {
+				eprintln!("got shutdown! breaking from loop");
+				break;
+			}
+			Ok(Meow::Tcp {
+				tx: tcp_tx_flag,
+				src,
+				dst,
+				len,
+			}) => {
+				if tcp_tx_flag {
+					tcp_tx[src as usize] += len;
+				} else {
+					tcp_rx[dst as usize] += len;
+				}
+			}
+			_ => (),
+		}
+	}
+}