diff --git a/Cargo.lock b/Cargo.lock
index 0170448..efca02a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,450 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "backtrace"
+version = "0.3.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cmake"
+version = "0.1.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[package]]
+name = "futures"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "libc"
+version = "0.2.169"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "nl-sys"
+version = "0.1.0"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "packets"
+version = "0.1.0"
+
+[[package]]
+name = "pcap-sys"
+version = "0.1.0"
+dependencies = [
+ "cmake",
+ "errno",
+ "fs_extra",
+ "futures",
+ "libc",
+ "packets",
+ "tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.93"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "sparse-beacon"
version = "0.7.0"
+dependencies = [
+ "nl-sys",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.96"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tokio"
+version = "1.43.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
+dependencies = [
+ "backtrace",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
diff --git a/Cargo.toml b/Cargo.toml
index 35853d1..d98d8e2 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,9 @@
[workspace]
members = [
- "sparse-beacon"
+ "sparse-beacon",
+ "nl-sys",
+ "packets",
+ "pcap-sys"
]
resolver = "2"
package.version = "2.0.0"
diff --git a/flake.nix b/flake.nix
index 0513595..26f2939 100644
--- a/flake.nix
+++ b/flake.nix
@@ -45,7 +45,8 @@
};
system-libs = import ./system-libs.nix {
- inherit pkgs libpcap-src winpcap-installer freebsd-libs-packed;
+ inherit pkgs libnl-src libpcap-src winpcap-installer
+ freebsd-libs-packed;
};
inherit (system-libs)
bsd-toolchain winpcap-drivers freebsd-libs libnl libpcap-linux
@@ -91,8 +92,9 @@
SPARSE_BUILD_WINPCAP_LIBS = "${winpcap-libs}/Lib";
SPARSE_BUILD_WINPCAP_DRIVERS = "${winpcap-drivers}/npf.sys";
- SPARSE_BUILD_LIBPCAP = libpcap-src;
- SPARSE_BUILD_LIBNL = libnl-src;
+ SPARSE_BUILD_LIBPCAP_LINUX = libpcap-linux;
+ SPARSE_BUILD_LIBPCAP_FREEBSD = libpcap-freebsd;
+ SPARSE_BUILD_LIBNL = libnl;
FREEBSD_LIBS = freebsd-libs;
diff --git a/nl-sys/Cargo.toml b/nl-sys/Cargo.toml
new file mode 100644
index 0000000..38ceb9b
--- /dev/null
+++ b/nl-sys/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "nl-sys"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+libc = "0.2.169"
+
+[build-dependencies]
+cc = "1.0"
diff --git a/nl-sys/README.md b/nl-sys/README.md
new file mode 100644
index 0000000..8945ac1
--- /dev/null
+++ b/nl-sys/README.md
@@ -0,0 +1,3 @@
+# nl-sys
+
+This library provides Rust wrappers around the netlink socket protocol that the Linux kernel uses in order to provide access to routing tables and interface information
\ No newline at end of file
diff --git a/nl-sys/build.rs b/nl-sys/build.rs
new file mode 100644
index 0000000..be894bf
--- /dev/null
+++ b/nl-sys/build.rs
@@ -0,0 +1,30 @@
+// Copyright (C) 2025 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+fn main() -> std::io::Result<()> {
+ cc::Build::new()
+ .compiler("clang")
+ .file("src/bridge.c")
+ .compile("bridge");
+
+ println!(
+ "cargo:rustc-link-search=native={}/lib",
+ std::env::var("SPARSE_BUILD_LIBNL").unwrap()
+ );
+ println!("cargo:rustc-link-lib=static=nl-3");
+ println!("cargo:rustc-link-lib=static=nl-route-3");
+
+ Ok(())
+}
diff --git a/nl-sys/src/bridge.c b/nl-sys/src/bridge.c
new file mode 100644
index 0000000..1b46b74
--- /dev/null
+++ b/nl-sys/src/bridge.c
@@ -0,0 +1,20 @@
+/**
+ * Copyright (C) 2023 Andrew Rioux
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+#include
+
+int netlink_route() { return NETLINK_ROUTE; }
diff --git a/nl-sys/src/error.rs b/nl-sys/src/error.rs
new file mode 100644
index 0000000..cd54973
--- /dev/null
+++ b/nl-sys/src/error.rs
@@ -0,0 +1,48 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::{ffi::CStr, fmt::Display};
+
+use libc::c_int;
+
+use crate::nl_ffi::nl_geterror;
+
+#[derive(Debug)]
+#[repr(transparent)]
+pub struct Error {
+ error_code: c_int,
+}
+
+impl Error {
+ pub(crate) fn new(error_code: c_int) -> Self {
+ Error { error_code }
+ }
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let error_msg_utf8 = unsafe {
+ let error_msg = nl_geterror(self.error_code);
+ let error_msg_ptr = CStr::from_ptr(error_msg);
+ std::str::from_utf8(error_msg_ptr.to_bytes()).unwrap()
+ };
+
+ write!(f, "internal libnl error: {error_msg_utf8}")
+ }
+}
+
+impl std::error::Error for Error {}
+
+pub type Result = std::result::Result;
diff --git a/nl-sys/src/lib.rs b/nl-sys/src/lib.rs
new file mode 100644
index 0000000..4510b54
--- /dev/null
+++ b/nl-sys/src/lib.rs
@@ -0,0 +1,24 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+pub mod error;
+pub mod netlink;
+pub mod nl_ffi;
+pub mod route;
+
+// from bridge.c
+extern "C" {
+ pub(crate) fn netlink_route() -> libc::c_int;
+}
diff --git a/nl-sys/src/netlink.rs b/nl-sys/src/netlink.rs
new file mode 100644
index 0000000..814fec3
--- /dev/null
+++ b/nl-sys/src/netlink.rs
@@ -0,0 +1,202 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::{marker::PhantomData, ptr};
+
+use libc::{AF_INET, AF_UNSPEC};
+
+use crate::{
+ error,
+ nl_ffi::*,
+ route::{Link, Neigh, Route, RtAddr},
+};
+
+/// A netlink socket used to communicate with the kernel
+pub struct Socket {
+ pub(crate) sock: *mut nl_sock,
+}
+
+impl Socket {
+ /// Establish a new connection with the Linux kernel
+ pub fn new() -> error::Result {
+ unsafe {
+ let sock = Socket {
+ sock: nl_socket_alloc(),
+ };
+
+ let ret = nl_connect(sock.sock, crate::netlink_route());
+ if ret < 0 {
+ return Err(error::Error::new(ret));
+ }
+
+ Ok(sock)
+ }
+ }
+
+ pub fn get_links(&self) -> error::Result> {
+ unsafe {
+ let mut link_cache = ptr::null_mut::();
+
+ let ret = rtnl_link_alloc_cache(self.sock, AF_UNSPEC, &mut link_cache as *mut _);
+
+ if ret < 0 {
+ return Err(error::Error::new(ret));
+ }
+
+ Ok(Cache {
+ cache: link_cache,
+ dt: PhantomData,
+ })
+ }
+ }
+
+ pub fn get_neigh(&self) -> error::Result> {
+ unsafe {
+ let mut neigh_cache = ptr::null_mut::();
+
+ let ret = rtnl_neigh_alloc_cache(self.sock, &mut neigh_cache as *mut _);
+
+ if ret < 0 {
+ return Err(error::Error::new(ret));
+ }
+
+ Ok(Cache {
+ cache: neigh_cache,
+ dt: PhantomData,
+ })
+ }
+ }
+
+ pub fn get_routes(&self) -> error::Result> {
+ unsafe {
+ let mut route_cache = ptr::null_mut::();
+
+ let ret = rtnl_route_alloc_cache(self.sock, AF_INET, 0, &mut route_cache as *mut _);
+
+ if ret < 0 {
+ return Err(error::Error::new(ret));
+ }
+
+ Ok(Cache {
+ cache: route_cache,
+ dt: PhantomData,
+ })
+ }
+ }
+
+ pub fn get_addrs(&self) -> error::Result> {
+ unsafe {
+ let mut addr_cache = ptr::null_mut::();
+
+ let ret = rtnl_addr_alloc_cache(self.sock, &mut addr_cache as *mut _);
+
+ if ret < 0 {
+ return Err(error::Error::new(ret));
+ }
+
+ Ok(Cache {
+ cache: addr_cache,
+ dt: PhantomData,
+ })
+ }
+ }
+}
+
+impl Drop for Socket {
+ fn drop(&mut self) {
+ unsafe {
+ nl_close(self.sock);
+ }
+ }
+}
+
+/// Tries to get a link by the specified ifindex
+pub fn get_link_by_index(cache: &Cache, index: i32) -> Option {
+ unsafe {
+ let link = rtnl_link_get(cache.cache, index);
+
+ if link.is_null() {
+ return None;
+ }
+
+ Some(Link { link })
+ }
+}
+
+/// Represents the nl_cache in the libnl library, which is itself a general
+/// collection of nl_objects
+pub struct Cache
+where
+ T: From<*mut nl_object>,
+{
+ pub(crate) cache: *mut nl_cache,
+ dt: PhantomData,
+}
+
+impl> Cache {
+ pub fn iter(&self) -> CacheIter<'_, T> {
+ let cache_size = unsafe { nl_cache_nitems(self.cache) } as usize;
+
+ CacheIter {
+ obj: unsafe { nl_cache_get_first(self.cache) },
+ cache_size,
+ index: 0,
+ item_type: PhantomData {},
+ }
+ }
+}
+
+impl> Drop for Cache {
+ fn drop(&mut self) {
+ unsafe {
+ nl_cache_put(self.cache);
+ }
+ }
+}
+
+/// Iterates over caches and provides an easy way to work with them
+pub struct CacheIter<'a, T> {
+ obj: *mut nl_object,
+ cache_size: usize,
+ index: usize,
+ item_type: PhantomData<&'a T>,
+}
+
+impl> Iterator for CacheIter<'_, T> {
+ type Item = T;
+
+ fn next(&mut self) -> Option {
+ loop {
+ if self.index >= self.cache_size {
+ return None;
+ }
+
+ self.index += 1;
+
+ let obj = self.obj;
+ self.obj = unsafe { nl_cache_get_next(obj) };
+
+ if obj.is_null() {
+ continue;
+ }
+
+ break Some(T::from(obj));
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option) {
+ (self.cache_size, Some(self.cache_size))
+ }
+}
diff --git a/nl-sys/src/nl_ffi.rs b/nl-sys/src/nl_ffi.rs
new file mode 100644
index 0000000..fbb362d
--- /dev/null
+++ b/nl-sys/src/nl_ffi.rs
@@ -0,0 +1,111 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use libc::{c_char, c_int, c_uint, c_void};
+
+macro_rules! nl_obj {
+ ($name:ident) => {
+ #[repr(C)]
+ #[allow(non_camel_case_types)]
+ pub struct $name {
+ _data: [u8; 0],
+ _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
+ }
+ };
+}
+
+nl_obj!(nl_sock);
+nl_obj!(nl_cache);
+nl_obj!(nl_addr);
+nl_obj!(nl_object);
+nl_obj!(nl_list_head);
+nl_obj!(rtnl_addr);
+nl_obj!(rtnl_link);
+nl_obj!(rtnl_neigh);
+nl_obj!(rtnl_route);
+nl_obj!(rtnl_nexthop);
+nl_obj!(flnl_request);
+
+// from libnl and libnl-route
+extern "C" {
+ pub fn nl_socket_alloc() -> *mut nl_sock;
+ pub fn nl_socket_free(sock: *mut nl_sock);
+ pub fn nl_socket_get_local_port(sock: *const nl_sock) -> u32;
+ pub fn nl_connect(sock: *mut nl_sock, protocol: c_int) -> c_int;
+ pub fn nl_close(sock: *mut nl_sock) -> c_void;
+ pub fn nl_geterror(error: c_int) -> *const c_char;
+
+ pub fn nl_object_put(obj: *mut nl_object) -> c_void;
+
+ pub fn nl_addr_get_len(addr: *mut nl_addr) -> c_uint;
+ pub fn nl_addr_get_binary_addr(addr: *mut nl_addr) -> *mut c_void;
+ pub fn nl_addr_parse(addrstr: *const i8, hint: c_int, result: *mut *mut nl_addr) -> c_int;
+ pub fn nl_addr_put(addr: *mut nl_addr) -> c_void;
+ pub fn nl_addr_get_family(addr: *mut nl_addr) -> c_int;
+ pub fn nl_addr_get_prefixlen(addr: *mut nl_addr) -> c_uint;
+
+ pub fn nl_cache_foreach(
+ cache: *mut nl_cache,
+ cb: extern "C" fn(*mut nl_object, *mut c_void),
+ arg: *mut c_void,
+ ) -> c_void;
+ pub fn nl_cache_put(cache: *mut nl_cache) -> c_void;
+ pub fn nl_cache_nitems(cache: *mut nl_cache) -> c_int;
+ pub fn nl_cache_get_first(cache: *mut nl_cache) -> *mut nl_object;
+ pub fn nl_cache_get_next(obj: *mut nl_object) -> *mut nl_object;
+ pub fn nl_cache_destroy_and_free(obj: *mut nl_cache) -> c_void;
+
+ pub fn rtnl_addr_alloc_cache(sock: *mut nl_sock, result: *mut *mut nl_cache) -> c_int;
+ pub fn rtnl_addr_get_ifindex(addr: *mut rtnl_addr) -> c_int;
+ pub fn rtnl_addr_get_family(addr: *mut rtnl_addr) -> c_int;
+ pub fn rtnl_addr_get_local(addr: *mut rtnl_addr) -> *mut nl_addr;
+
+ pub fn rtnl_neigh_alloc_cache(sock: *mut nl_sock, result: *mut *mut nl_cache) -> c_int;
+ pub fn rtnl_neigh_get(
+ cache: *mut nl_cache,
+ ifindex: c_int,
+ dst: *mut nl_addr,
+ ) -> *mut rtnl_neigh;
+ pub fn rtnl_neigh_get_dst(neigh: *mut rtnl_neigh) -> *mut nl_addr;
+ pub fn rtnl_neigh_get_lladdr(neigh: *mut rtnl_neigh) -> *mut nl_addr;
+ pub fn rtnl_neigh_get_ifindex(neigh: *mut rtnl_neigh) -> c_int;
+
+ pub fn rtnl_link_get(cache: *mut nl_cache, index: c_int) -> *mut rtnl_link;
+ pub fn rtnl_link_alloc_cache(
+ sock: *mut nl_sock,
+ family: c_int,
+ result: *mut *mut nl_cache,
+ ) -> c_int;
+ pub fn rtnl_link_get_addr(link: *mut rtnl_link) -> *mut nl_addr;
+ pub fn rtnl_link_get_name(link: *mut rtnl_link) -> *const c_char;
+ pub fn rtnl_link_get_ifindex(link: *mut rtnl_link) -> c_int;
+ pub fn rtnl_link_get_type(link: *mut rtnl_link) -> *const c_char;
+
+ pub fn rtnl_route_alloc_cache(
+ sock: *mut nl_sock,
+ family: c_int,
+ flags: c_int,
+ result: *mut *mut nl_cache,
+ ) -> c_int;
+ pub fn rtnl_route_get_src(route: *mut rtnl_route) -> *mut nl_addr;
+ pub fn rtnl_route_get_dst(route: *mut rtnl_route) -> *mut nl_addr;
+ pub fn rtnl_route_get_iif(route: *mut rtnl_route) -> c_int;
+ pub fn rtnl_route_get_pref_src(route: *mut rtnl_route) -> *mut nl_addr;
+ pub fn rtnl_route_get_nnexthops(route: *mut rtnl_route) -> c_int;
+ pub fn rtnl_route_nexthop_n(route: *mut rtnl_route, ind: c_int) -> *mut rtnl_nexthop;
+
+ pub fn rtnl_route_nh_get_gateway(hop: *mut rtnl_nexthop) -> *mut nl_addr;
+ pub fn rtnl_route_nh_get_ifindex(hop: *mut rtnl_nexthop) -> c_int;
+}
diff --git a/nl-sys/src/route.rs b/nl-sys/src/route.rs
new file mode 100644
index 0000000..9fbd512
--- /dev/null
+++ b/nl-sys/src/route.rs
@@ -0,0 +1,590 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::{
+ ffi::{CStr, CString},
+ fmt::Debug,
+ net::Ipv4Addr,
+};
+
+use libc::{c_int, c_uint, AF_INET, AF_LLC};
+
+use crate::{
+ error,
+ netlink::{self, Cache},
+};
+
+use super::nl_ffi::*;
+
+/// Represents an address assigned to a link
+pub struct RtAddr {
+ addr: *mut rtnl_addr,
+}
+
+impl RtAddr {
+ pub fn local(&self) -> Option {
+ unsafe {
+ let addr = rtnl_addr_get_local(self.addr);
+
+ if addr.is_null() {
+ return None;
+ }
+
+ Some(Addr { addr })
+ }
+ }
+
+ pub fn ifindex(&self) -> i32 {
+ unsafe { rtnl_addr_get_ifindex(self.addr) }
+ }
+
+ pub fn family(&self) -> i32 {
+ unsafe { rtnl_addr_get_family(self.addr) }
+ }
+}
+
+impl From<*mut nl_object> for RtAddr {
+ fn from(value: *mut nl_object) -> Self {
+ RtAddr {
+ addr: value as *mut _,
+ }
+ }
+}
+
+/// Represents a network link, which can represent a network device
+pub struct Link {
+ pub(crate) link: *mut rtnl_link,
+}
+
+impl Link {
+ /// Returns the network link name, e.g. eth0
+ pub fn name(&self) -> String {
+ unsafe {
+ let name = rtnl_link_get_name(self.link);
+ if name.is_null() {
+ return "".to_string();
+ }
+ let name_rs = CStr::from_ptr(name);
+ std::str::from_utf8(name_rs.to_bytes()).unwrap().to_owned()
+ }
+ }
+
+ /// Provides the address of the link. Can change based on the type of link,
+ /// representing MAC addresses or IP addresses
+ pub fn addr(&self) -> Addr {
+ unsafe {
+ Addr {
+ addr: rtnl_link_get_addr(self.link),
+ }
+ }
+ }
+
+ /// Determines the type of link. Ethernet devices are "veth or eth"
+ pub fn ltype(&self) -> Option {
+ unsafe {
+ let ltype = rtnl_link_get_type(self.link);
+ if ltype.is_null() {
+ return None;
+ }
+ let ltype_rs = CStr::from_ptr(ltype);
+ Some(std::str::from_utf8(ltype_rs.to_bytes()).ok()?.to_owned())
+ }
+ }
+
+ /// Determines the index of the interface in the kernel table
+ pub fn ifindex(&self) -> c_int {
+ unsafe { rtnl_link_get_ifindex(self.link) }
+ }
+
+ /// Tries to get the neighbor for this link, which can provide the destination address and the
+ /// link layer address (lladdr)
+ pub fn get_neigh(&self, neigh_table: &Cache, addr: &Addr) -> Option<[u8; 6]> {
+ unsafe {
+ let neigh = rtnl_neigh_get(neigh_table.cache, self.ifindex(), addr.addr);
+
+ if neigh.is_null() {
+ return None;
+ }
+
+ Neigh { neigh }.lladdr().hw_address().try_into().ok()
+ }
+ }
+}
+
+impl Debug for Link {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Link")
+ .field("name", &self.name())
+ .field("ifindex", &self.ifindex())
+ .finish()
+ }
+}
+
+impl From<*mut nl_object> for Link {
+ fn from(value: *mut nl_object) -> Self {
+ Self {
+ link: value as *mut _,
+ }
+ }
+}
+
+pub fn get_macs_and_src_for_ip(
+ addrs: &Cache,
+ routes: &Cache,
+ neighs: &Cache,
+ links: &Cache,
+ addr: Ipv4Addr,
+) -> Option<(String, i32, Ipv4Addr, [u8; 6], [u8; 6], u8)> {
+ let mut sorted_routes = routes.iter().collect::>();
+
+ sorted_routes.sort_by(|r1, r2| {
+ r2.dst()
+ .map(|a| a.cidrlen())
+ .partial_cmp(&r1.dst().map(|a| a.cidrlen()))
+ .unwrap_or(std::cmp::Ordering::Equal)
+ });
+
+ let ip_int = u32::from(addr);
+
+ let route = sorted_routes.iter().find(|route| {
+ let Some(dst) = route.dst() else { return false };
+
+ let mask = if dst.cidrlen() != 0 {
+ (0xFFFFFFFFu32.overflowing_shr(32 - dst.cidrlen()))
+ .0
+ .overflowing_shl(32 - dst.cidrlen())
+ .0
+ } else {
+ 0
+ };
+
+ let Ok(dst_addr): Result = (&dst).try_into() else {
+ return false;
+ };
+ let dst_addr: u32 = dst_addr.into();
+
+ (mask & dst_addr) == (mask & ip_int)
+ })?;
+
+ let link_ind = route.hop_iter().next()?.ifindex();
+
+ #[cfg(debug_assertions)]
+ {
+ println!("Link index: {link_ind}\n");
+ for link in links.iter() {
+ println!(
+ "Link {}: {:?} ({})",
+ link.name(),
+ link.addr(),
+ link.ifindex()
+ );
+
+ println!("\tAddrs:");
+ for addr in addrs.iter().filter(|addr| addr.ifindex() == link.ifindex()) {
+ if let Some(a) = addr.local() {
+ println!("\t\t{:?}", a)
+ }
+ }
+
+ println!("\tNeighbors:");
+ for neigh in neighs
+ .iter()
+ .filter(|neigh| neigh.ifindex() == link.ifindex())
+ {
+ println!("\t\t{:?}, {:?}", neigh.dst(), neigh.lladdr());
+ }
+ }
+ }
+
+ let link = netlink::get_link_by_index(links, link_ind)?;
+
+ let neigh = neighs
+ .iter()
+ .find(|n| n.ifindex() == link.ifindex())
+ .map(|n| n.lladdr().hw_address().try_into().ok())
+ .flatten()
+ .unwrap_or([0xFFu8; 6]);
+
+ let srcip = addrs.iter().find(|a| a.ifindex() == link.ifindex())?;
+
+ Some((
+ link.name(),
+ link_ind,
+ (&srcip.local()?).try_into().ok()?,
+ link.addr().hw_address().try_into().ok()?,
+ neigh,
+ route.dst().unwrap().cidrlen() as u8,
+ ))
+}
+
+/// Gets the neighbor record for the source IP specified, or get the default address
+pub fn get_neigh_for_addr(
+ routes: &Cache,
+ neighs: &Cache,
+ links: &Cache,
+ addr: &Addr,
+) -> Option<(Ipv4Addr, Link, [u8; 6])> {
+ for link in links.iter() {
+ let Some(neigh) = link.get_neigh(&neighs, addr) else {
+ continue;
+ };
+ return Some((addr.try_into().ok()?, link, neigh));
+ }
+
+ // No good neighbors were found above, try to use the default address
+ if let Some(def_neigh) = get_default_route(routes) {
+ println!("Found default route, trying to get link for it");
+ if let Some((laddr, link, neigh)) = neighs
+ .iter()
+ .filter_map(|n| {
+ let Some(link) = netlink::get_link_by_index(links, n.ifindex()) else {
+ return None;
+ };
+
+ let Some(first_hop) = def_neigh.hop_iter().next() else {
+ return None;
+ };
+
+ if n.ifindex() != first_hop.ifindex() {
+ return None;
+ }
+
+ Some(((&first_hop.gateway()?).try_into().ok()?, link, n.lladdr()))
+ })
+ .next()
+ {
+ return Some((laddr, link, neigh.hw_address().try_into().ok()?));
+ }
+ }
+
+ None
+}
+
+/// Given the routes cache, returns the default route among them
+pub fn get_default_route(routes: &Cache) -> Option {
+ routes
+ .iter()
+ .find(|r| r.dst().map(|a| a.cidrlen()).unwrap_or(33) == 0)
+}
+
+/// A struct representing the neighbor of a link
+pub struct Neigh {
+ neigh: *mut rtnl_neigh,
+}
+
+impl Neigh {
+ /// Pull up the destination address for this neighbor record
+ pub fn dst(&self) -> Addr {
+ unsafe {
+ let addr = rtnl_neigh_get_dst(self.neigh);
+ Addr { addr }
+ }
+ }
+
+ // Bring up the link local address for the neighbor link
+ pub fn lladdr(&self) -> Addr {
+ unsafe {
+ let addr = rtnl_neigh_get_lladdr(self.neigh);
+ Addr { addr }
+ }
+ }
+
+ pub fn ifindex(&self) -> i32 {
+ unsafe { rtnl_neigh_get_ifindex(self.neigh) }
+ }
+}
+
+impl From<*mut nl_object> for Neigh {
+ fn from(value: *mut nl_object) -> Self {
+ Self {
+ neigh: value as *mut _,
+ }
+ }
+}
+
+/// Represents "an address"
+/// IPv4? IPv6? MAC? Whatever the "any" or "lo" devices use? Yes!
+pub struct Addr {
+ addr: *mut nl_addr,
+}
+
+impl Addr {
+ /// Returns the number of bytes that are in the address
+ pub fn len(&self) -> u32 {
+ unsafe { nl_addr_get_len(self.addr) }
+ }
+
+ /// Returns the address, which can be interpreted based on the results of [`Addr::atype`]
+ pub fn hw_address(&self) -> Vec {
+ unsafe {
+ let hw_address_ptr = nl_addr_get_binary_addr(self.addr) as *const u8;
+ let hw_address_slice = std::slice::from_raw_parts(hw_address_ptr, self.len() as usize);
+
+ hw_address_slice.to_vec()
+ }
+ }
+
+ // Determines the type of data in [`Addr::hw_address`]
+ pub fn atype(&self) -> Option {
+ if self.addr.is_null() {
+ None
+ } else {
+ Some(unsafe { nl_addr_get_family(self.addr) })
+ }
+ }
+
+ /// Returns the length of the subnet mask applying to this address
+ pub fn cidrlen(&self) -> c_uint {
+ unsafe { nl_addr_get_prefixlen(self.addr) }
+ }
+}
+
+impl Debug for Addr {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let res = match self.atype() {
+ Some(AF_INET) => {
+ let octets = self.hw_address();
+ f.debug_struct("Addr")
+ .field(
+ "addr",
+ &format!(
+ "{}.{}.{}.{}/{}",
+ octets[0],
+ octets[1],
+ octets[2],
+ octets[3],
+ self.cidrlen()
+ ),
+ )
+ .finish()
+ }
+ Some(AF_LLC) => {
+ let octets = self.hw_address();
+
+ f.debug_struct("Addr")
+ .field(
+ "addr",
+ &format!(
+ "{:02X?}:{:02X?}:{:02X?}:{:02X?}:{:02X?}:{:02X?}",
+ octets[0], octets[1], octets[2], octets[3], octets[4], octets[5],
+ ),
+ )
+ .finish()
+ }
+ None => f
+ .debug_struct("Addr")
+ .field("addr", &"unknown")
+ .field("atype", &"unknown")
+ .finish(),
+ _ => f
+ .debug_struct("Addr")
+ .field("addr", &self.hw_address())
+ .field("atype", &self.atype())
+ .finish(),
+ };
+ res
+ }
+}
+
+impl From for Addr {
+ fn from(value: Ipv4Addr) -> Self {
+ unsafe {
+ let mut addr = std::ptr::null_mut::();
+ let value = CString::new(format!("{value}")).unwrap();
+
+ // we can ignore the return code because it is guaranteed to not be invalid
+ nl_addr_parse(value.as_ptr(), AF_INET, &mut addr as *mut _);
+
+ Addr { addr }
+ }
+ }
+}
+
+impl TryFrom<&Addr> for Ipv4Addr {
+ type Error = error::Error;
+
+ fn try_from(value: &Addr) -> Result {
+ if value.len() != 4 {
+ return Err(error::Error::new(15 /* NL_AF_MISMATCH */));
+ }
+
+ let addr = value.hw_address();
+ Ok(Ipv4Addr::new(addr[0], addr[1], addr[2], addr[3]))
+ }
+}
+
+/// Represents a route in the kernel routing table
+pub struct Route {
+ route: *mut rtnl_route,
+}
+
+impl Route {
+ /// Represents the destination of the route
+ pub fn src(&self) -> Option {
+ unsafe {
+ let addr = rtnl_route_get_src(self.route);
+
+ if addr.is_null() {
+ return None;
+ }
+
+ Some(Addr { addr })
+ }
+ }
+
+ /// Represents the destination of the route
+ pub fn dst(&self) -> Option {
+ unsafe {
+ let addr = rtnl_route_get_dst(self.route);
+
+ if addr.is_null() {
+ return None;
+ }
+
+ Some(Addr { addr })
+ }
+ }
+
+ /// Returns the amount of hops are in this route
+ pub fn nexthop_len(&self) -> c_int {
+ unsafe { rtnl_route_get_nnexthops(self.route) }
+ }
+
+ /// Gets the hop at the index specify
+ pub fn nexthop(&self, ind: i32) -> Option {
+ unsafe {
+ let nexthop = rtnl_route_nexthop_n(self.route, ind);
+ if nexthop.is_null() {
+ return None;
+ }
+ Some(Nexthop { nexthop })
+ }
+ }
+
+ /// Returns an iterator representing all the hops for this route
+ pub fn hop_iter(&self) -> NexthopIter<'_> {
+ NexthopIter {
+ route: &self,
+ index: 0,
+ }
+ }
+}
+
+impl From<*mut nl_object> for Route {
+ fn from(value: *mut nl_object) -> Self {
+ Route {
+ route: value as *mut _,
+ }
+ }
+}
+
+/// Represents the hops of a network route
+pub struct Nexthop {
+ nexthop: *mut rtnl_nexthop,
+}
+
+impl Nexthop {
+ /// Returns the gateway used for this network hop
+ pub fn gateway(&self) -> Option {
+ unsafe {
+ let addr = rtnl_route_nh_get_gateway(self.nexthop);
+
+ if addr.is_null() {
+ return None;
+ }
+
+ Some(Addr { addr })
+ }
+ }
+
+ /// Returns the interface index for this network hop
+ pub fn ifindex(&self) -> i32 {
+ unsafe { rtnl_route_nh_get_ifindex(self.nexthop) }
+ }
+}
+
+/// An iterator for working with route hops
+pub struct NexthopIter<'a> {
+ route: &'a Route,
+ index: i32,
+}
+
+impl Iterator for NexthopIter<'_> {
+ type Item = Nexthop;
+
+ fn next(&mut self) -> Option {
+ let next = self.route.nexthop(self.index);
+
+ if next.is_none() {
+ return None;
+ }
+
+ self.index += 1;
+
+ next
+ }
+
+ fn size_hint(&self) -> (usize, Option) {
+ (
+ self.route.nexthop_len() as usize,
+ Some(self.route.nexthop_len() as usize),
+ )
+ }
+}
+
+/// Determines the source IP address to use in order to make a network request
+pub fn get_srcip_for_dstip(routes: &Cache, ip: Ipv4Addr) -> Option {
+ let mut sorted_routes = routes.iter().collect::>();
+
+ sorted_routes.sort_by(|r1, r2| {
+ r2.dst()
+ .map(|a| a.cidrlen())
+ .partial_cmp(&r1.dst().map(|a| a.cidrlen()))
+ .unwrap_or(std::cmp::Ordering::Equal)
+ });
+
+ let ip_int = u32::from(ip);
+
+ sorted_routes
+ .iter()
+ .filter(|route| {
+ let Some(dst) = route.dst() else { return false };
+
+ let mask = if dst.cidrlen() != 0 {
+ (0xFFFFFFFFu32.overflowing_shr(32 - dst.cidrlen()))
+ .0
+ .overflowing_shl(32 - dst.cidrlen())
+ .0
+ } else {
+ 0
+ };
+
+ let Ok(dst_addr): Result = (&dst).try_into() else {
+ return false;
+ };
+ let dst_addr: u32 = dst_addr.into();
+
+ (mask & dst_addr) == (mask & ip_int)
+ })
+ .filter_map(|route| {
+ route
+ .hop_iter()
+ .next()
+ .and_then(|hop| hop.gateway())
+ .or(route.dst())
+ })
+ .filter_map(|gateway| (&gateway).try_into().ok())
+ .next()
+}
diff --git a/packets/Cargo.toml b/packets/Cargo.toml
new file mode 100644
index 0000000..ff4201f
--- /dev/null
+++ b/packets/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "packets"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
diff --git a/packets/src/error.rs b/packets/src/error.rs
new file mode 100644
index 0000000..8413e5c
--- /dev/null
+++ b/packets/src/error.rs
@@ -0,0 +1,40 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::{error, fmt::Display};
+
+#[derive(Debug)]
+pub enum Error {
+ StringParse,
+ UnknownPacketType(u16),
+ PacketLengthInvalid,
+}
+
+pub type Result = std::result::Result;
+
+impl Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::StringParse => write!(f, "unable to parse a string from pcap"),
+ Error::UnknownPacketType(ptype) => write!(f, "unknown packet type ({ptype})"),
+ Error::PacketLengthInvalid => write!(
+ f,
+ "received a packet with a length that mismatched the header"
+ ),
+ }
+ }
+}
+
+impl error::Error for Error {}
diff --git a/packets/src/lib.rs b/packets/src/lib.rs
new file mode 100644
index 0000000..a5f7da4
--- /dev/null
+++ b/packets/src/lib.rs
@@ -0,0 +1,806 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::{
+ net::Ipv4Addr,
+ sync::atomic::{AtomicU16, Ordering},
+};
+
+pub mod error;
+
+pub struct EthernetPkt<'a> {
+ pub data: &'a [u8],
+}
+
+impl<'a> EthernetPkt<'a> {
+ pub fn destination_address(&self) -> &[u8; 6] {
+ self.data[0..6].try_into().unwrap()
+ }
+
+ pub fn source_address(&self) -> &[u8; 6] {
+ self.data[6..12].try_into().unwrap()
+ }
+
+ pub fn ether_type(&self) -> u16 {
+ u16::from_be_bytes(self.data[12..14].try_into().unwrap())
+ }
+
+ pub fn get_layer3_pkt(&'_ self) -> error::Result> {
+ if self.data.len() < 14 {
+ return Err(error::Error::PacketLengthInvalid);
+ }
+
+ match self.ether_type() {
+ 0x0800 => {
+ if self.data.len() < 34 {
+ return Err(error::Error::PacketLengthInvalid);
+ }
+
+ let pkt = IPv4Pkt {
+ data: &self.data[14..],
+ };
+
+ if self.data.len() < (14 + pkt.total_length()).into() {
+ return Err(error::Error::PacketLengthInvalid);
+ }
+
+ Ok(Layer3Pkt::IPv4Pkt(pkt))
+ }
+ 0x0806 => {
+ if self.data.len() < 42 {
+ return Err(error::Error::PacketLengthInvalid);
+ }
+
+ let pkt = ARPPkt {
+ data: &self.data[14..],
+ };
+
+ Ok(Layer3Pkt::ARP(pkt))
+ }
+ p => Err(error::Error::UnknownPacketType(p)),
+ }
+ }
+
+ pub fn to_owned(&self) -> EthernetPacket {
+ EthernetPacket {
+ data: self.data.to_vec(),
+ }
+ }
+}
+
+pub enum Layer3Pkt<'a> {
+ IPv4Pkt(IPv4Pkt<'a>),
+ ARP(ARPPkt<'a>),
+}
+
+pub struct IPv4Pkt<'a> {
+ data: &'a [u8],
+}
+
+impl<'a> IPv4Pkt<'a> {
+ pub fn version(&self) -> u8 {
+ 4
+ }
+
+ pub fn header_len(&self) -> u8 {
+ 4 * (0x0F & self.data[0])
+ }
+
+ pub fn type_of_service(&self) -> u8 {
+ self.data[1] >> 2
+ }
+
+ pub fn ecn(&self) -> u8 {
+ self.data[1] & 0x03
+ }
+
+ pub fn total_length(&self) -> u16 {
+ u16::from_be_bytes(self.data[2..4].try_into().unwrap())
+ }
+
+ pub fn id(&self) -> u16 {
+ u16::from_be_bytes(self.data[4..6].try_into().unwrap())
+ }
+
+ pub fn dont_fragment(&self) -> bool {
+ self.data[6] & 0x40 != 0
+ }
+
+ pub fn more_fragments(&self) -> bool {
+ self.data[6] & 0x80 != 0
+ }
+
+ pub fn fragment_offset(&self) -> u16 {
+ u16::from_be_bytes([self.data[6] & 0x1F, self.data[7]]) * 8
+ }
+
+ pub fn ttl(&self) -> u8 {
+ self.data[8]
+ }
+
+ pub fn protocol(&self) -> u8 {
+ self.data[9]
+ }
+
+ pub fn header_checksum(&self) -> u16 {
+ u16::from_be_bytes(self.data[10..12].try_into().unwrap())
+ }
+
+ pub fn source_ip(&self) -> Ipv4Addr {
+ <&[u8] as TryInto<[u8; 4]>>::try_into(&self.data[12..16])
+ .unwrap()
+ .into()
+ }
+
+ pub fn dest_ip(&self) -> Ipv4Addr {
+ <&[u8] as TryInto<[u8; 4]>>::try_into(&self.data[16..20])
+ .unwrap()
+ .into()
+ }
+
+ pub fn to_owned(&self) -> IPv4Packet {
+ IPv4Packet {
+ data: self.data.to_vec(),
+ }
+ }
+
+ pub fn get_layer4_packet(&self) -> error::Result> {
+ match self.protocol() {
+ // ICMP
+ 0x1 => Err(error::Error::UnknownPacketType(0x1)),
+ // TCP
+ 0x6 => Ok(Layer4Pkt::TCP(TCPPkt {
+ data: &self.data[(self.header_len() as usize)..],
+ })),
+ 0x11 => Ok(Layer4Pkt::UDP(UDPPkt {
+ data: &self.data[(self.header_len() as usize)..],
+ })),
+ p => Err(error::Error::UnknownPacketType(p.into())),
+ }
+ }
+}
+
+pub struct ARPPkt<'a> {
+ data: &'a [u8],
+}
+
+impl<'a> ARPPkt<'a> {
+ pub fn htype(&self) -> u16 {
+ u16::from_be_bytes(self.data[0..2].try_into().unwrap())
+ }
+
+ pub fn ptype(&self) -> u16 {
+ u16::from_be_bytes(self.data[2..4].try_into().unwrap())
+ }
+
+ pub fn hwlen(&self) -> u8 {
+ self.data[4]
+ }
+
+ pub fn plen(&self) -> u8 {
+ self.data[5]
+ }
+
+ pub fn opcode(&self) -> u16 {
+ u16::from_be_bytes(self.data[6..8].try_into().unwrap())
+ }
+
+ pub fn srchwaddr(&self) -> &[u8] {
+ &self.data[8usize..8usize + self.hwlen() as usize]
+ }
+
+ pub fn srcprotoaddr(&self) -> &[u8] {
+ let start = self.hwlen() as usize + 8;
+ &self.data[start..start + self.plen() as usize]
+ }
+
+ pub fn targethwaddr(&self) -> &[u8] {
+ let start = self.hwlen() as usize + self.plen() as usize + 8;
+ &self.data[start..start + self.hwlen() as usize]
+ }
+
+ pub fn targetprotoaddr(&self) -> &[u8] {
+ let start = self.hwlen() as usize + self.plen() as usize + self.hwlen() as usize + 8;
+ &self.data[start..start + self.plen() as usize]
+ }
+}
+
+pub enum Layer4Pkt<'a> {
+ UDP(UDPPkt<'a>),
+ TCP(TCPPkt<'a>),
+}
+
+impl<'a> Layer4Pkt<'a> {
+ pub fn len(&self) -> u16 {
+ match self {
+ Layer4Pkt::UDP(pkt) => pkt.len(),
+ Layer4Pkt::TCP(pkt) => pkt.data.len() as u16,
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+}
+
+pub struct UDPPkt<'a> {
+ data: &'a [u8],
+}
+
+impl<'a> UDPPkt<'a> {
+ pub fn srcport(&self) -> u16 {
+ u16::from_be_bytes(self.data[0..2].try_into().unwrap())
+ }
+
+ pub fn dstport(&self) -> u16 {
+ u16::from_be_bytes(self.data[2..4].try_into().unwrap())
+ }
+
+ pub fn len(&self) -> u16 {
+ u16::from_be_bytes(self.data[4..6].try_into().unwrap())
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ pub fn to_owned(&self) -> UDPPacket {
+ UDPPacket {
+ data: self.data.to_vec(),
+ }
+ }
+
+ pub fn get_data(&'_ self) -> &'_ [u8] {
+ &self.data[8..(self.len() as usize)]
+ }
+}
+
+pub struct TCPPkt<'a> {
+ data: &'a [u8],
+}
+
+impl<'a> TCPPkt<'a> {
+ pub fn len(&self) -> u8 {
+ (self.data[12] >> 4) * 4
+ }
+
+ pub fn srcport(&self) -> u16 {
+ u16::from_be_bytes(self.data[0..2].try_into().unwrap())
+ }
+
+ pub fn dstport(&self) -> u16 {
+ u16::from_be_bytes(self.data[2..4].try_into().unwrap())
+ }
+
+ pub fn seqnumber(&self) -> u32 {
+ u32::from_be_bytes(self.data[4..8].try_into().unwrap())
+ }
+
+ pub fn acknumber(&self) -> u32 {
+ u32::from_be_bytes(self.data[8..12].try_into().unwrap())
+ }
+
+ pub fn flags(&self) -> u8 {
+ self.data[13]
+ }
+
+ pub fn cwr(&self) -> bool {
+ self.flags() & 0x80 != 0
+ }
+
+ pub fn ece(&self) -> bool {
+ self.flags() & 0x40 != 0
+ }
+
+ pub fn urg(&self) -> bool {
+ self.flags() & 0x20 != 0
+ }
+
+ pub fn ack(&self) -> bool {
+ self.flags() & 0x10 != 0
+ }
+
+ pub fn psh(&self) -> bool {
+ self.flags() & 0x08 != 0
+ }
+
+ pub fn rst(&self) -> bool {
+ self.flags() & 0x04 != 0
+ }
+
+ pub fn syn(&self) -> bool {
+ self.flags() & 0x02 != 0
+ }
+
+ pub fn fin(&self) -> bool {
+ self.flags() & 0x01 != 0
+ }
+
+ pub fn window(&self) -> u16 {
+ u16::from_be_bytes(self.data[14..16].try_into().unwrap())
+ }
+
+ pub fn checksum(&self) -> u16 {
+ u16::from_be_bytes(self.data[16..18].try_into().unwrap())
+ }
+
+ pub fn urgent_ptr(&self) -> u16 {
+ u16::from_be_bytes(self.data[18..20].try_into().unwrap())
+ }
+
+ pub fn options(&self) -> &[u8] {
+ &self.data[20..(self.len() as usize)]
+ }
+
+ pub fn data(&self) -> &[u8] {
+ &self.data[(self.len() as usize)..]
+ }
+
+ pub fn verify_checksum(&self, srcip: I1, dstip: I2) -> bool
+ where
+ I1: Into,
+ I2: Into,
+ {
+ let source = srcip.into();
+ let dest = dstip.into();
+
+ let protocol = &[0x00u8, 0x06u8];
+
+ let tcp_length = (self.data.len()) as u16;
+
+ let len: u8 = (self.len()).try_into().unwrap();
+ let len = len << 4;
+
+ let bytes = [
+ &source.octets()[..],
+ &dest.octets(),
+ protocol,
+ &tcp_length.to_be_bytes(),
+ &self.srcport().to_be_bytes()[..],
+ &self.dstport().to_be_bytes(),
+ &self.seqnumber().to_be_bytes(),
+ &self.acknumber().to_be_bytes(),
+ &[len],
+ &[self.flags()],
+ &self.window().to_be_bytes(),
+ &[0, 0],
+ &self.urgent_ptr().to_be_bytes(),
+ &self.options(),
+ &self.data(),
+ ]
+ .concat();
+
+ let checksum = bytes
+ .chunks(2)
+ .map(|pair| {
+ if pair.len() == 1 {
+ (pair[0] as u32) << 8
+ } else {
+ (pair[0] as u32) << 8 | (pair[1] as u32)
+ }
+ })
+ .fold(0u32, |acc, e| acc + e)
+ + 30;
+ // + 30 to intentionally deviate from
+ // the RFC, to make it so that I can identify the packets as sparse
+ // packets
+
+ let checksum = (checksum >> 16) + (checksum & 0xffff);
+ let checksum = ((checksum >> 16) as u16) + (checksum as u16);
+
+ checksum == self.checksum()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct EthernetPacket {
+ data: Vec,
+}
+
+impl EthernetPacket {
+ pub fn construct(src: [u8; 6], dst: [u8; 6], packet: &Layer3Packet) -> EthernetPacket {
+ match packet {
+ Layer3Packet::IPv4(pkt) => EthernetPacket {
+ data: [&dst[..], &src[..], &[0x08_u8, 0x00_u8][..], &*pkt.data].concat(),
+ },
+ Layer3Packet::ARP(pkt) => EthernetPacket {
+ data: [&dst[..], &src[..], &[0x08_u8, 0x06_u8][..], &*pkt.data].concat(),
+ },
+ }
+ }
+
+ #[inline]
+ pub fn pkt(&'_ self) -> EthernetPkt<'_> {
+ EthernetPkt { data: &self.data }
+ }
+}
+
+#[derive(Clone)]
+pub enum Layer3Packet {
+ IPv4(IPv4Packet),
+ ARP(ARPPacket),
+}
+
+#[derive(Clone)]
+pub struct ARPPacket {
+ data: Vec,
+}
+
+pub enum ARPMode {
+ Request,
+ Reply,
+}
+
+impl ARPMode {
+ fn opcode(&self) -> u16 {
+ match self {
+ Self::Request => 1,
+ Self::Reply => 2,
+ }
+ }
+}
+
+pub enum ARPProto {
+ IPv4,
+}
+
+impl ARPProto {
+ fn opcode(&self) -> u16 {
+ match self {
+ Self::IPv4 => 0x0800,
+ }
+ }
+}
+
+impl ARPPacket {
+ pub fn construct(
+ op: ARPMode,
+ proto: ARPProto,
+ srchwaddr: &[u8],
+ targethwaddr: &[u8],
+ srcprotoaddr: &[u8],
+ targetprotoaddr: &[u8],
+ ) -> ARPPacket {
+ assert!(srchwaddr.len() == targethwaddr.len());
+ assert!(srcprotoaddr.len() == targetprotoaddr.len());
+
+ let data = [
+ &0x0001u16.to_be_bytes()[..],
+ &proto.opcode().to_be_bytes()[..],
+ &[srchwaddr.len() as u8],
+ &[srcprotoaddr.len() as u8],
+ &op.opcode().to_be_bytes(),
+ srchwaddr,
+ srcprotoaddr,
+ targethwaddr,
+ targetprotoaddr,
+ ]
+ .concat();
+
+ ARPPacket { data }
+ }
+}
+
+static IPV4_ID: AtomicU16 = AtomicU16::new(0xabcd);
+
+#[derive(Clone)]
+pub struct IPv4Packet {
+ pub data: Vec,
+}
+
+impl IPv4Packet {
+ pub fn construct(source: I1, dest: I2, packet: &Layer4Packet) -> IPv4Packet
+ where
+ I1: Into,
+ I2: Into,
+ {
+ let source = source.into();
+ let dest = dest.into();
+
+ let dscpen: u8 = 0;
+
+ let totallen: u16 = 20 + packet.pkt().len();
+ let id: u16 = IPV4_ID.fetch_add(1, Ordering::SeqCst);
+
+ let fragmentation: u16 = 0x4000;
+
+ let ttl: u8 = 64;
+ let protocol: u8 = match packet {
+ Layer4Packet::UDP(_) => 0x11,
+ Layer4Packet::TCP(_) => 0x6,
+ };
+
+ let source_upper = u16::from_be_bytes(source.octets()[0..2].try_into().unwrap());
+ let source_lower = u16::from_be_bytes(source.octets()[2..4].try_into().unwrap());
+ let dest_upper = u16::from_be_bytes(dest.octets()[0..2].try_into().unwrap());
+ let dest_lower = u16::from_be_bytes(dest.octets()[2..4].try_into().unwrap());
+
+ let versihldscpen = 0x4500 | dscpen as u16;
+ let ttlprotocol = ((ttl as u32) << 8_u8) | protocol as u32;
+
+ let checksum_part: u32 = versihldscpen as u32
+ + totallen as u32
+ + id as u32
+ + ttlprotocol
+ + fragmentation as u32
+ + source_upper as u32
+ + source_lower as u32
+ + dest_upper as u32
+ + dest_lower as u32;
+
+ let checksum: u16 = (((checksum_part & 0xFFFF) + (checksum_part >> 16)) & 0xFFFF)
+ .try_into()
+ .unwrap();
+
+ let checksum = 0xFFFF - checksum;
+
+ let data = [
+ &versihldscpen.to_be_bytes()[..],
+ &totallen.to_be_bytes(),
+ &id.to_be_bytes(),
+ &fragmentation.to_be_bytes(),
+ &[ttl, protocol],
+ &checksum.to_be_bytes(),
+ &source.octets(),
+ &dest.octets(),
+ match packet {
+ Layer4Packet::UDP(pkt) => &*pkt.data,
+ Layer4Packet::TCP(pkt) => &*pkt.data,
+ },
+ ]
+ .concat();
+
+ IPv4Packet { data }
+ }
+
+ #[inline]
+ pub fn pkt(&'_ self) -> IPv4Pkt<'_> {
+ IPv4Pkt { data: &self.data }
+ }
+}
+
+#[derive(Clone)]
+pub enum Layer4Packet {
+ UDP(UDPPacket),
+ TCP(TCPPacket),
+}
+
+impl Layer4Packet {
+ pub fn pkt(&'_ self) -> Layer4Pkt<'_> {
+ match self {
+ Layer4Packet::UDP(pkt) => Layer4Pkt::UDP(pkt.pkt()),
+ Layer4Packet::TCP(pkt) => Layer4Pkt::TCP(pkt.pkt()),
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct UDPPacket {
+ data: Vec,
+}
+
+impl UDPPacket {
+ pub fn construct(source: u16, dest: u16, data: T) -> UDPPacket
+ where
+ T: Into>,
+ {
+ let data = data.into();
+
+ UDPPacket {
+ data: [
+ &source.to_be_bytes(),
+ &dest.to_be_bytes(),
+ &(8_u16 + TryInto::::try_into(data.len()).unwrap()).to_be_bytes(),
+ &[0_u8, 0_u8],
+ &*data,
+ ]
+ .concat(),
+ }
+ }
+
+ #[inline]
+ pub fn pkt(&'_ self) -> UDPPkt<'_> {
+ UDPPkt { data: &self.data }
+ }
+}
+
+#[derive(Clone)]
+pub struct TCPPacket {
+ data: Vec,
+}
+
+impl TCPPacket {
+ #[inline]
+ pub fn pkt(&'_ self) -> TCPPkt<'_> {
+ TCPPkt { data: &self.data }
+ }
+}
+
+#[derive(Default, Debug)]
+pub struct TCPPacketBuilder {
+ srcport: Option,
+ dstport: Option,
+ seqnumber: Option,
+ acknumber: u32,
+ flags: u8,
+ window: Option,
+ urgent_ptr: u16,
+ options: Vec,
+}
+
+macro_rules! declare_flag {
+ ($name:ident, $offset:expr) => {
+ pub fn $name(mut self, enable: bool) -> Self {
+ if enable {
+ self.flags |= $offset
+ } else {
+ self.flags &= (0xFF ^ $offset)
+ }
+ self
+ }
+ };
+}
+
+impl TCPPacketBuilder {
+ pub fn srcport(mut self, port: u16) -> Self {
+ self.srcport = Some(port);
+ self
+ }
+
+ pub fn dstport(mut self, port: u16) -> Self {
+ self.dstport = Some(port);
+ self
+ }
+
+ pub fn seqnumber(mut self, num: u32) -> Self {
+ self.seqnumber = Some(num);
+ self
+ }
+
+ pub fn acknumber(mut self, num: u32) -> Self {
+ self.acknumber = num;
+ self
+ }
+
+ declare_flag!(cwr, 0x80);
+ declare_flag!(ece, 0x40);
+ declare_flag!(urg, 0x20);
+ declare_flag!(ack, 0x10);
+ declare_flag!(psh, 0x08);
+ declare_flag!(rst, 0x04);
+ declare_flag!(syn, 0x02);
+ declare_flag!(fin, 0x01);
+
+ pub fn window(mut self, window: u16) -> Self {
+ self.window = Some(window);
+ self
+ }
+
+ pub fn urgent_ptr(mut self, ptr: u16) -> Self {
+ self.urgent_ptr = ptr;
+ self
+ }
+
+ pub fn options(mut self, opts: Vec) -> Self {
+ self.options = opts;
+ self
+ }
+
+ pub fn build(
+ self,
+ srcip: I1,
+ dstip: I2,
+ data: Vec,
+ checksum_offset: Option,
+ ) -> TCPPacket
+ where
+ I1: Into,
+ I2: Into,
+ {
+ let source = srcip.into();
+ let dest = dstip.into();
+
+ let protocol = &[0x00u8, 0x06u8];
+
+ let tcp_length = (data.len() + self.options.len() + 20) as u16;
+
+ let srcport = self.srcport.unwrap();
+ let dstport = self.dstport.unwrap();
+ let seqnumber = self.seqnumber.unwrap();
+ let window = self.window.unwrap();
+
+ let mut options = self.options;
+ while options.len() % 4 != 0 {
+ options.push(0);
+ }
+
+ let len: u8 = (options.len() / 4 + 5).try_into().unwrap();
+ let len = len << 4;
+
+ let mut bytes = [
+ &source.octets()[..],
+ &dest.octets(),
+ protocol,
+ &tcp_length.to_be_bytes(),
+ &srcport.to_be_bytes()[..],
+ &dstport.to_be_bytes(),
+ &seqnumber.to_be_bytes(),
+ &self.acknumber.to_be_bytes(),
+ &[len],
+ &[self.flags],
+ &window.to_be_bytes(),
+ &[0, 0],
+ &self.urgent_ptr.to_be_bytes(),
+ &options,
+ &data,
+ ]
+ .concat();
+
+ let checksum = bytes
+ .chunks(2)
+ .map(|pair| {
+ if pair.len() == 1 {
+ (pair[0] as u32) << 8
+ } else {
+ (pair[0] as u32) << 8 | (pair[1] as u32)
+ }
+ })
+ .fold(0u32, |acc, e| acc + e)
+ + (checksum_offset.unwrap_or(0) as u32);
+
+ let checksum = (checksum >> 16) + (checksum & 0xffff);
+ let checksum = ((checksum >> 16) as u16) + (checksum as u16);
+ let checksum = (!checksum).to_be_bytes();
+
+ //bytes[16] = checksum[0];
+ //bytes[17] = checksum[1];
+ bytes[28] = checksum[0];
+ bytes[29] = checksum[1];
+
+ TCPPacket {
+ data: bytes[12..].to_vec(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use std::net::Ipv4Addr;
+
+ use crate::TCPPacketBuilder;
+
+ #[test]
+ fn test_checksum() {
+ let tcp_packet = TCPPacketBuilder::default()
+ .srcport(57116)
+ .dstport(54248)
+ .seqnumber(0xaed77262)
+ .acknumber(0)
+ .syn(true)
+ .window(64240)
+ .options(vec![
+ 0x02, 0x04, 0x05, 0xb4, 0x04, 0x02, 0x08, 0x0a, 0xd4, 0x06, 0xdf, 0x18, 0x00, 0x00,
+ 0x00, 0x00, 0x01, 0x03, 0x03, 0x07,
+ ])
+ .build(
+ Ipv4Addr::new(10, 104, 18, 67),
+ Ipv4Addr::new(10, 104, 21, 16),
+ vec![],
+ None,
+ );
+
+ assert_eq!(tcp_packet.pkt().checksum(), 0x898d);
+ }
+}
diff --git a/pcap-sys/Cargo.toml b/pcap-sys/Cargo.toml
new file mode 100644
index 0000000..d434a94
--- /dev/null
+++ b/pcap-sys/Cargo.toml
@@ -0,0 +1,31 @@
+# Copyright (C) 2023 Andrew Rioux
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+[package]
+name = "pcap-sys"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+errno = "0.3.10"
+futures = "0.3.25"
+libc = "0.2.169"
+tokio = { version = "1.21.2", features = ["net", "rt", "macros", "rt-multi-thread" ] }
+tokio-stream = "0.1.14"
+packets = { path = "../packets" }
+
+[build-dependencies]
+cmake = "0.1"
+fs_extra = "1.3.0"
diff --git a/pcap-sys/README.md b/pcap-sys/README.md
new file mode 100644
index 0000000..58efa1f
--- /dev/null
+++ b/pcap-sys/README.md
@@ -0,0 +1,3 @@
+# pcap-sys
+
+This library provides wrappers around libpcap that when built in the dev container environment allow for full static linking against libpcap
\ No newline at end of file
diff --git a/pcap-sys/build.rs b/pcap-sys/build.rs
new file mode 100644
index 0000000..465abe7
--- /dev/null
+++ b/pcap-sys/build.rs
@@ -0,0 +1,78 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use fs_extra::dir::{copy, CopyOptions};
+
+fn main() {
+ if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" {
+ println!("cargo:rustc-link-search=native={}", std::env::var("SPARSE_BUILD_WINPCAP").unwrap());
+ println!("cargo:rustc-link-search=native={}/x64", std::env::var("SPARSE_BUILD_WINPCAP").unwrap());
+ println!("cargo:rustc-link-lib=wpcap");
+ } else if std::env::var("CARGO_CFG_TARGET_OS").unwrap() == "freebsd" {
+ let libpcap_src = format!("{}/pcap_src", std::env::var("OUT_DIR").unwrap());
+
+ let mut options = CopyOptions::new();
+ options.copy_inside = true;
+ options.skip_exist = true;
+ copy(std::env::var("SPARSE_BUILD_LIBPCAP").unwrap(), &libpcap_src, &options).expect("could not copy libpcap source code to build");
+
+ let dst = cmake::Config::new(&libpcap_src)
+ .profile("MinSizeRel")
+ .define("BUILD_SHARED_LIBS", "OFF")
+ .define("DISABLE_BLUETOOTH", "ON")
+ .define("DISABLE_DAG", "ON")
+ .define("DISABLE_DBUS", "ON")
+ .define("DISABLE_DPDK", "ON")
+ .define("DISABLE_NETMAP", "ON")
+ .define("DISABLE_RDMA", "ON")
+ .define("DISABLE_SEPTEL", "ON")
+ .define("DISABLE_SNF", "ON")
+ .define("DISABLE_TC", "ON")
+ .build();
+
+ println!("cargo:rustc-link-search=native={}/lib", dst.display());
+ println!("cargo:rustc-link-search=native={}/lib64", dst.display());
+ println!("cargo:rustc-link-lib=static=pcap");
+ } else {
+ let libpcap_src = format!("{}/pcap_src", std::env::var("OUT_DIR").unwrap());
+
+ let mut options = CopyOptions::new();
+ options.copy_inside = true;
+ options.skip_exist = true;
+ copy(std::env::var("SPARSE_BUILD_LIBPCAP").unwrap(), &libpcap_src, &options).expect("could not copy libpcap source code to build");
+
+ let dst = cmake::Config::new(&libpcap_src)
+ .profile("MinSizeRel")
+ .define("BUILD_SHARED_LIBS", "OFF")
+ .define("BUILD_WITH_LIBNL", "OFF")
+ .define("DISABLE_BLUETOOTH", "ON")
+ .define("DISABLE_DAG", "ON")
+ .define("DISABLE_DBUS", "ON")
+ .define("DISABLE_DPDK", "ON")
+ .define("DISABLE_LINUX_USBMON", "ON")
+ .define("DISABLE_NETMAP", "ON")
+ .define("DISABLE_RDMA", "ON")
+ .define("DISABLE_SEPTEL", "ON")
+ .define("DISABLE_SNF", "ON")
+ .define("DISABLE_TC", "ON")
+ .define("PCAP_TYPE", "linux")
+ .build();
+
+ // panic!("hahahahah test {}", dst.display());
+ println!("cargo:rustc-link-search=native={}/lib", dst.display());
+ println!("cargo:rustc-link-search=native={}/lib64", dst.display());
+ println!("cargo:rustc-link-lib=static=pcap");
+ }
+}
diff --git a/pcap-sys/src/error.rs b/pcap-sys/src/error.rs
new file mode 100644
index 0000000..fad274f
--- /dev/null
+++ b/pcap-sys/src/error.rs
@@ -0,0 +1,136 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use errno::Errno;
+use std::{
+ convert::From,
+ error,
+ ffi::{self, CStr, CString},
+ fmt::Display,
+};
+
+#[derive(Debug)]
+pub enum Error {
+ PcapError(CString),
+ PcapErrorIf(String, CString),
+ StringParse,
+ UnknownPacketType(u16),
+ PacketLengthInvalid,
+ InvalidPcapFd,
+ Io(std::io::Error),
+ Libc(Errno),
+}
+
+pub type Result = std::result::Result;
+
+impl Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Error::PcapError(err) => {
+ if let Ok(err_str) = std::str::from_utf8(err.as_bytes()) {
+ return write!(f, "pcap error: {err_str}");
+ }
+
+ write!(f, "unknown pcap error")
+ }
+ Error::PcapErrorIf(ifname, err) => {
+ if let Ok(err_str) = std::str::from_utf8(err.as_bytes()) {
+ return write!(f, "pcap error on interface {ifname}: {err_str}");
+ }
+
+ write!(f, "unknown pcap error with interface {ifname}")
+ }
+ Error::StringParse => write!(f, "unable to parse a string from pcap"),
+ Error::UnknownPacketType(ptype) => write!(f, "unknown packet type ({ptype})"),
+ Error::PacketLengthInvalid => write!(
+ f,
+ "received a packet with a length that mismatched the header"
+ ),
+ Error::InvalidPcapFd => write!(f, "internal pcap file descriptor error"),
+ Error::Io(io) => write!(f, "std::io error ({io})"),
+ Error::Libc(err) => write!(f, "libc error ({err})"),
+ }
+ }
+}
+
+impl Error {
+ pub fn add_ifname(self, ifname: &str) -> Self {
+ match self {
+ Error::PcapError(err) => Error::PcapErrorIf(ifname.to_string(), err),
+ other => other,
+ }
+ }
+}
+
+impl error::Error for Error {
+ fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+ match self {
+ Error::Io(err) => Some(err),
+ Error::Libc(err) => Some(err),
+ _ => None,
+ }
+ }
+}
+
+impl From<&[i8; 256]> for Error {
+ fn from(buf: &[i8; 256]) -> Error {
+ match CString::new(
+ buf.iter()
+ .take_while(|&&v| v != 0)
+ .map(|&v| v as u8)
+ .collect::>(),
+ ) {
+ Ok(s) => Error::PcapError(s),
+ Err(_) => Error::StringParse,
+ }
+ }
+}
+
+impl From<*const i8> for Error {
+ #[allow(clippy::not_unsafe_ptr_arg_deref)]
+ fn from(buf: *const i8) -> Error {
+ Error::PcapError(unsafe { CStr::from_ptr(buf) }.to_owned())
+ }
+}
+
+impl From for Error {
+ fn from(_err: ffi::NulError) -> Error {
+ Error::StringParse
+ }
+}
+
+impl From for Error {
+ fn from(err: std::io::Error) -> Error {
+ Error::Io(err)
+ }
+}
+
+impl From for Error {
+ fn from(err: Errno) -> Error {
+ Error::Libc(err)
+ }
+}
+
+impl From for Error {
+ fn from(err: packets::error::Error) -> Error {
+ use packets::error::Error as ExtE;
+
+ match err {
+ ExtE::PacketLengthInvalid => Error::PacketLengthInvalid,
+ ExtE::UnknownPacketType(t) => Error::UnknownPacketType(t),
+ ExtE::StringParse => Error::StringParse,
+ }
+ }
+}
diff --git a/pcap-sys/src/ffi.rs b/pcap-sys/src/ffi.rs
new file mode 100644
index 0000000..bb406e0
--- /dev/null
+++ b/pcap-sys/src/ffi.rs
@@ -0,0 +1,183 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use libc::{c_char, c_int, c_uchar, c_uint, c_ushort, c_void};
+
+pub const DLT_NULL: i32 = 0;
+pub const DLT_EN10MB: i32 = 1;
+pub const DLT_EN3MB: i32 = 2;
+pub const DLT_AX25: i32 = 3;
+pub const DLT_PRONET: i32 = 4;
+pub const DLT_CHAOS: i32 = 5;
+pub const DLT_IEEE802: i32 = 6;
+pub const DLT_ARCNET: i32 = 7;
+pub const DLT_SLIP: i32 = 8;
+pub const DLT_PPP: i32 = 9;
+pub const DLT_FDDI: i32 = 10;
+
+pub const PCAP_VERSION_MAJOR: c_int = 2;
+pub const PCAP_VERSION_MINOR: c_int = 4;
+pub const PCAP_ERRBUF_SIZE: usize = 256;
+pub const PCAP_IF_LOOPBACK: c_int = 0x00000001;
+pub const PCAP_IF_UP: c_int = 0x00000002;
+pub const PCAP_IF_RUNNING: c_int = 0x00000004;
+pub const PCAP_IF_WIRELESS: c_int = 0x00000008;
+pub const PCAP_IF_CONNECTION_STATUS: c_int = 0x00000030;
+pub const PCAP_IF_CONNECTION_STATUS_UNKNOWN: c_int = 0x00000000;
+pub const PCAP_IF_CONNECTION_STATUS_CONNECTED: c_int = 0x00000010;
+pub const PCAP_IF_CONNECTION_STATUS_DISCONNECTED: c_int = 0x00000020;
+pub const PCAP_IF_CONNECTION_STATUS_NOT_APPLICABLE: c_int = 0x00000030;
+pub const PCAP_ERROR: c_int = -1;
+pub const PCAP_ERROR_BREAK: c_int = -2;
+pub const PCAP_ERROR_NOT_ACTIVATED: c_int = -3;
+pub const PCAP_ERROR_ACTIVATED: c_int = -4;
+pub const PCAP_ERROR_NO_SUCH_DEVICE: c_int = -5;
+pub const PCAP_ERROR_RFMON_NOTSUP: c_int = -6;
+pub const PCAP_ERROR_NOT_RFMON: c_int = -7;
+pub const PCAP_ERROR_PERM_DENIED: c_int = -8;
+pub const PCAP_ERROR_IFACE_NOT_UP: c_int = -9;
+pub const PCAP_ERROR_CANTSET_TSTAMP_TYPE: c_int = 10;
+pub const PCAP_ERROR_PROMISC_PERM_DENIED: c_int = -11;
+pub const PCAP_ERROR_TSTAMP_PRECISION_NOTSUP: c_int = -12;
+pub const PCAP_WARNING: c_int = 1;
+pub const PCAP_WARNING_PROMISC_NOTSUP: c_int = 2;
+pub const PCAP_WARNING_TSTAMP_TYPE_NOTSUP: c_int = 3;
+pub const PCAP_NETMASK_UNKNOWN: u32 = 0xffffffff;
+pub const PCAP_CHAR_ENC_LOCAL: c_int = 0x00000000;
+pub const PCAP_CHAR_ENC_UTF_8: c_int = 0x00000001;
+pub const PCAP_TSTAMP_HOST: c_int = 0;
+pub const PCAP_TSTAMP_HOST_LOWPREC: c_int = 1;
+pub const PCAP_TSTAMP_HOST_HIPREC: c_int = 2;
+pub const PCAP_TSTAMP_ADAPTER: c_int = 3;
+pub const PCAP_TSTAMP_ADAPTER_UNSYNCED: c_int = 4;
+pub const PCAP_TSTAMP_HOST_HIPREC_UNSYNCED: c_int = 5;
+pub const PCAP_TSTAMP_PRECISION_MICRO: c_int = 0;
+pub const PCAP_TSTAMP_PRECISION_NANO: c_int = 1;
+pub const PCAP_BUF_SIZE: c_int = 1024;
+pub const PCAP_SRC_FILE: c_int = 2;
+pub const PCAP_SRC_IFLOCAL: c_int = 3;
+pub const PCAP_SRC_IFREMOTE: c_int = 4;
+pub const PCAP_SRC_FILE_STRING: &[u8] = b"file://";
+pub const PCAP_SRC_IF_STRING: &[u8] = b"rpcap://";
+pub const PCAP_OPENFLAG_PROMISCUOUS: c_int = 0x00000001;
+pub const PCAP_OPENFLAG_DATATX_UDP: c_int = 0x00000002;
+pub const PCAP_OPENFLAG_NOCAPTURE_RPCAP: c_int = 0x00000004;
+pub const PCAP_OPENFLAG_NOCAPTURE_LOCAL: c_int = 0x00000008;
+pub const PCAP_OPENFLAG_MAX_RESPONSIVENESS: c_int = 0x00000010;
+pub const RPCAP_RMTAUTH_NULL: c_int = 0;
+pub const RPCAP_RMTAUTH_PWD: c_int = 1;
+pub const PCAP_SAMP_NOSAMP: c_int = 0;
+pub const PCAP_SAMP_1_EVERY_N: c_int = 1;
+pub const PCAP_SAMP_FIRST_AFTER_N_MS: c_int = 2;
+pub const RPCAP_HOSTLIST_SIZE: c_int = 1024;
+
+pub const BUFSIZ: c_int = 8192;
+
+#[repr(C)]
+pub struct PcapDev {
+ _data: [u8; 0],
+}
+
+#[repr(C)]
+pub struct SockAddr {
+ pub sa_family: c_ushort,
+ pub sa_data: [u8; 14],
+}
+
+#[repr(C)]
+pub struct PcapAddr {
+ pub next: *const PcapAddr,
+ pub addr: *const SockAddr,
+ pub netmask: *const SockAddr,
+ pub broadaddr: *const SockAddr,
+ pub dstaddr: *const SockAddr,
+}
+
+#[repr(C)]
+pub struct PcapDevIf {
+ pub next: *const PcapDevIf,
+ pub name: *const c_char,
+ pub description: *const c_char,
+ pub addresses: *const PcapAddr,
+ pub flags: u32,
+}
+
+#[repr(C)]
+pub struct bpf_insn {
+ code: c_ushort,
+ jt: c_uchar,
+ jf: c_uchar,
+ k: u32,
+}
+
+#[repr(C)]
+pub struct BpfProgram {
+ pub bf_len: c_uint,
+ pub bpf_insn: *const bpf_insn,
+}
+
+#[repr(C)]
+pub struct PktHeader {
+ pub ts: libc::timeval,
+ pub caplen: u32,
+ pub len: u32,
+}
+
+// #[link(name = "pcap", kind = "static", modifiers = "+whole-archive")]
+extern "C" {
+ pub fn pcap_findalldevs(pcap_dev_if: *mut *const PcapDevIf, errbuf: *mut c_char) -> c_int;
+ pub fn pcap_freealldevs(pcap_dev_if: *const PcapDevIf);
+ pub fn pcap_create(source: *const c_char, errbuf: *mut c_char) -> *mut PcapDev;
+ pub fn pcap_close(p: *mut PcapDev);
+ pub fn pcap_set_promisc(dev: *mut PcapDev, promisc: c_int) -> c_int;
+ pub fn pcap_set_buffer_size(dev: *mut PcapDev, bufsize: c_int) -> c_int;
+ pub fn pcap_set_timeout(dev: *mut PcapDev, ms: c_int) -> c_int;
+ pub fn pcap_activate(dev: *mut PcapDev) -> c_int;
+ pub fn pcap_datalink(dev: *mut PcapDev) -> c_int;
+ pub fn pcap_geterr(dev: *mut PcapDev) -> *const c_char;
+ pub fn pcap_lookupnet(
+ dev: *const c_char,
+ net: *mut u32,
+ mask: *mut u32,
+ errbuf: *mut c_char,
+ ) -> c_int;
+ pub fn pcap_compile(
+ dev: *mut PcapDev,
+ fp: *mut BpfProgram,
+ filter_exp: *const i8,
+ optimize: c_int,
+ mask: u32,
+ ) -> c_int;
+ pub fn pcap_setfilter(dev: *mut PcapDev, fp: *const BpfProgram) -> c_int;
+ pub fn pcap_loop(
+ p: *mut PcapDev,
+ cnt: c_int,
+ callback: unsafe extern "C" fn(
+ user: *mut c_void,
+ header: *const PktHeader,
+ data: *const u8,
+ ),
+ user: *mut c_void,
+ ) -> c_int;
+ pub fn pcap_breakloop(p: *mut PcapDev);
+ pub fn pcap_sendpacket(p: *mut PcapDev, buf: *const c_uchar, size: c_int) -> c_int;
+ pub fn pcap_setnonblock(dev: *mut PcapDev, nonblock: c_int, errbuf: *mut c_char) -> c_int;
+ pub fn pcap_get_selectable_fd(p: *mut PcapDev) -> c_int;
+ pub fn pcap_next_ex(
+ p: *mut PcapDev,
+ header: *mut *mut PktHeader,
+ pkt_data: *mut *mut c_char,
+ ) -> c_int;
+}
diff --git a/pcap-sys/src/lib.rs b/pcap-sys/src/lib.rs
new file mode 100644
index 0000000..c41403e
--- /dev/null
+++ b/pcap-sys/src/lib.rs
@@ -0,0 +1,418 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::{
+ ffi::{CStr, CString},
+ ptr, slice
+};
+
+pub mod error;
+mod ffi;
+pub use packets;
+#[cfg(target_os = "linux")]
+pub mod stream;
+
+pub mod consts {
+ pub use super::ffi::{
+ BUFSIZ, DLT_ARCNET, DLT_AX25, DLT_CHAOS, DLT_EN10MB, DLT_EN3MB, DLT_FDDI, DLT_IEEE802,
+ DLT_NULL, DLT_PPP, DLT_PRONET, DLT_SLIP, PCAP_BUF_SIZE, PCAP_CHAR_ENC_LOCAL,
+ PCAP_CHAR_ENC_UTF_8, PCAP_ERRBUF_SIZE, PCAP_ERROR, PCAP_ERROR_ACTIVATED, PCAP_ERROR_BREAK,
+ PCAP_ERROR_CANTSET_TSTAMP_TYPE, PCAP_ERROR_IFACE_NOT_UP, PCAP_ERROR_NOT_ACTIVATED,
+ PCAP_ERROR_NOT_RFMON, PCAP_ERROR_NO_SUCH_DEVICE, PCAP_ERROR_PERM_DENIED,
+ PCAP_ERROR_PROMISC_PERM_DENIED, PCAP_ERROR_RFMON_NOTSUP,
+ PCAP_ERROR_TSTAMP_PRECISION_NOTSUP, PCAP_IF_CONNECTION_STATUS,
+ PCAP_IF_CONNECTION_STATUS_CONNECTED, PCAP_IF_CONNECTION_STATUS_DISCONNECTED,
+ PCAP_IF_CONNECTION_STATUS_NOT_APPLICABLE, PCAP_IF_CONNECTION_STATUS_UNKNOWN,
+ PCAP_IF_LOOPBACK, PCAP_IF_RUNNING, PCAP_IF_UP, PCAP_IF_WIRELESS, PCAP_NETMASK_UNKNOWN,
+ PCAP_OPENFLAG_DATATX_UDP, PCAP_OPENFLAG_MAX_RESPONSIVENESS, PCAP_OPENFLAG_NOCAPTURE_LOCAL,
+ PCAP_OPENFLAG_NOCAPTURE_RPCAP, PCAP_OPENFLAG_PROMISCUOUS, PCAP_SAMP_1_EVERY_N,
+ PCAP_SAMP_FIRST_AFTER_N_MS, PCAP_SAMP_NOSAMP, PCAP_SRC_FILE, PCAP_SRC_FILE_STRING,
+ PCAP_SRC_IFLOCAL, PCAP_SRC_IFREMOTE, PCAP_SRC_IF_STRING, PCAP_TSTAMP_ADAPTER,
+ PCAP_TSTAMP_ADAPTER_UNSYNCED, PCAP_TSTAMP_HOST, PCAP_TSTAMP_HOST_HIPREC,
+ PCAP_TSTAMP_HOST_HIPREC_UNSYNCED, PCAP_TSTAMP_HOST_LOWPREC, PCAP_TSTAMP_PRECISION_MICRO,
+ PCAP_TSTAMP_PRECISION_NANO, PCAP_VERSION_MAJOR, PCAP_VERSION_MINOR, PCAP_WARNING,
+ PCAP_WARNING_PROMISC_NOTSUP, PCAP_WARNING_TSTAMP_TYPE_NOTSUP, RPCAP_HOSTLIST_SIZE,
+ RPCAP_RMTAUTH_NULL, RPCAP_RMTAUTH_PWD,
+ };
+}
+
+use ffi::PcapDevIf;
+
+pub struct PcapDevIterator {
+ dev_if: *const PcapDevIf,
+ current_dev: *const PcapDevIf,
+}
+
+impl PcapDevIterator {
+ pub fn new() -> error::Result {
+ let mut errbuf = [0i8; ffi::PCAP_ERRBUF_SIZE];
+ let mut dev_if = ptr::null();
+
+ if unsafe { ffi::pcap_findalldevs(&mut dev_if, errbuf.as_mut_ptr()) } != 0 {
+ Err(&errbuf)?;
+ }
+
+ Ok(PcapDevIterator {
+ dev_if,
+ current_dev: dev_if,
+ })
+ }
+}
+
+impl Drop for PcapDevIterator {
+ fn drop(&mut self) {
+ unsafe {
+ ffi::pcap_freealldevs(self.dev_if);
+ }
+ }
+}
+
+impl std::iter::Iterator for PcapDevIterator {
+ type Item = String;
+
+ fn next(&mut self) -> Option {
+ unsafe {
+ if self.current_dev.is_null() {
+ return None;
+ }
+
+ let current_dev = self.current_dev;
+
+ let dev_name = CStr::from_ptr((*current_dev).name)
+ .to_str()
+ .unwrap()
+ .to_string();
+
+ self.current_dev = (*current_dev).next;
+
+ Some(dev_name)
+ }
+ }
+}
+
+pub trait State {}
+pub trait Activated: State {}
+pub trait NotListening: Activated {}
+pub trait Listening: Activated {}
+pub trait Disabled: State {}
+
+pub enum DevActivated {}
+impl State for DevActivated {}
+impl Activated for DevActivated {}
+impl NotListening for DevActivated {}
+
+pub enum DevDisabled {}
+impl State for DevDisabled {}
+impl Disabled for DevDisabled {}
+
+pub enum DevListening {}
+impl State for DevListening {}
+impl Activated for DevListening {}
+impl Listening for DevListening {}
+
+pub struct BpfProgram {}
+
+pub struct Interface {
+ dev_name: CString,
+ dev: *mut ffi::PcapDev,
+ marker: std::marker::PhantomData,
+ absorbed: bool,
+ nonblocking: bool,
+}
+
+impl Drop for Interface {
+ fn drop(&mut self) {
+ if !self.absorbed {
+ unsafe { ffi::pcap_close(self.dev) };
+ }
+ }
+}
+
+unsafe impl Send for Interface {}
+unsafe impl Sync for Interface {}
+
+impl Interface {
+ pub fn new(name: &str) -> error::Result> {
+ let mut errbuf = [0i8; ffi::PCAP_ERRBUF_SIZE];
+
+ let dev_name = CString::new(name)?;
+ let dev = unsafe { ffi::pcap_create(dev_name.as_ptr(), errbuf.as_mut_ptr()) };
+
+ if dev.is_null() {
+ Err(&errbuf)?;
+ }
+
+ Ok(Interface:: {
+ dev_name,
+ dev,
+ marker: std::marker::PhantomData,
+ absorbed: false,
+ nonblocking: false,
+ })
+ }
+
+ pub fn lookupnet(&self) -> error::Result<(u32, u32)> {
+ let mut errbuf = [0i8; ffi::PCAP_ERRBUF_SIZE];
+ let mut net: u32 = 0;
+ let mut mask: u32 = 0;
+
+ if unsafe {
+ ffi::pcap_lookupnet(
+ self.dev_name.as_ptr(),
+ &mut net as *mut u32,
+ &mut mask as *mut u32,
+ errbuf.as_mut_ptr(),
+ )
+ } == -1
+ {
+ Err(error::Error::from(&errbuf))?;
+ }
+
+ Ok((net, mask))
+ }
+
+ pub fn set_non_blocking(&mut self, nonblocking: bool) -> error::Result<()> {
+ self.nonblocking = nonblocking;
+
+ let mut errbuf = [0i8; ffi::PCAP_ERRBUF_SIZE];
+
+ if unsafe { ffi::pcap_setnonblock(self.dev, i32::from(nonblocking), errbuf.as_mut_ptr()) }
+ == -1
+ {
+ Err(error::Error::from(&errbuf))?;
+ }
+
+ Ok(())
+ }
+
+ pub fn name(&self) -> &str {
+ std::str::from_utf8(self.dev_name.as_bytes()).unwrap()
+ }
+}
+
+impl Interface {
+ pub fn set_promisc(&mut self, promisc: bool) -> error::Result<()> {
+ if unsafe { ffi::pcap_set_promisc(self.dev, i32::from(promisc)) } != 0 {
+ Err(unsafe { ffi::pcap_geterr(self.dev) })?;
+ }
+
+ Ok(())
+ }
+
+ pub fn set_buffer_size(&mut self, bufsize: i32) -> error::Result<()> {
+ if unsafe { ffi::pcap_set_buffer_size(self.dev, bufsize) } != 0 {
+ Err(unsafe { ffi::pcap_geterr(self.dev) })?;
+ }
+
+ Ok(())
+ }
+
+ pub fn set_timeout(&mut self, timeout: i32) -> error::Result<()> {
+ if unsafe { ffi::pcap_set_timeout(self.dev, timeout) } != 0 {
+ Err(unsafe { ffi::pcap_geterr(self.dev) })?;
+ }
+
+ Ok(())
+ }
+
+ pub fn activate(mut self) -> error::Result> {
+ if unsafe { ffi::pcap_activate(self.dev) } != 0 {
+ Err(unsafe { ffi::pcap_geterr(self.dev) })?;
+ }
+
+ self.absorbed = true;
+
+ Ok(Interface:: {
+ dev_name: self.dev_name.clone(),
+ dev: self.dev,
+ marker: std::marker::PhantomData,
+ absorbed: false,
+ nonblocking: self.nonblocking,
+ })
+ }
+}
+
+impl Interface {
+ pub fn datalink(&self) -> i32 {
+ unsafe { ffi::pcap_datalink(self.dev) }
+ }
+
+ pub fn set_filter(
+ &self,
+ filter: &str,
+ optimize: bool,
+ mask: Option,
+ ) -> error::Result> {
+ let mut bpf = ffi::BpfProgram {
+ bf_len: 0,
+ bpf_insn: ptr::null(),
+ };
+
+ let mask = if let Some(m) = mask {
+ m
+ } else {
+ let (_, m) = self.lookupnet()?;
+ m
+ };
+
+ let filter = CString::new(filter)?;
+
+ if unsafe {
+ ffi::pcap_compile(
+ self.dev,
+ &mut bpf as *mut ffi::BpfProgram,
+ filter.as_ptr(),
+ i32::from(optimize),
+ mask,
+ )
+ } == -1
+ {
+ Err(unsafe { ffi::pcap_geterr(self.dev) })?;
+ }
+
+ if unsafe { ffi::pcap_setfilter(self.dev, &bpf as *const ffi::BpfProgram) } == -1 {
+ Err(unsafe { ffi::pcap_geterr(self.dev) })?;
+ }
+
+ Ok(Box::new(bpf))
+ }
+
+ pub fn sendpacket(&self, packet: packets::EthernetPkt) -> error::Result<()> {
+ if unsafe {
+ ffi::pcap_sendpacket(
+ self.dev,
+ packet.data.as_ptr(),
+ packet.data.len().try_into().unwrap(),
+ )
+ } == ffi::PCAP_ERROR
+ {
+ Err(unsafe { ffi::pcap_geterr(self.dev) })?;
+ }
+
+ Ok(())
+ }
+
+ pub fn next_packet(&mut self) -> error::Result {
+ let mut header: *mut ffi::PktHeader = ptr::null_mut();
+ let mut data: *mut libc::c_char = ptr::null_mut();
+
+ if unsafe { ffi::pcap_next_ex(self.dev, &mut header as *mut _, &mut data as *mut _) < 1 } {
+ return unsafe { Err(ffi::pcap_geterr(self.dev))? };
+ }
+
+ let rdata = unsafe { slice::from_raw_parts(data as *mut u8, (*header).caplen as usize) };
+ if rdata.len() < 24 {
+ return Err(error::Error::PacketLengthInvalid);
+ }
+
+ Ok(packets::EthernetPkt { data: rdata }.to_owned())
+ }
+}
+
+struct ListenHandler<'a, F>
+where
+ F: FnMut(&Interface, packets::EthernetPkt) -> error::Result,
+{
+ packet_handler: F,
+ break_on_fail: bool,
+ fail_error: Option,
+ interface: &'a Interface,
+}
+
+impl Interface {
+ pub fn listen(
+ &self,
+ packet_handler: F,
+ break_on_fail: bool,
+ packet_count: i32,
+ ) -> (Option, i32)
+ where
+ F: FnMut(&Interface, packets::EthernetPkt) -> error::Result,
+ {
+ unsafe extern "C" fn cback(
+ user: *mut libc::c_void,
+ header: *const ffi::PktHeader,
+ data: *const u8,
+ ) where
+ F: FnMut(&Interface, packets::EthernetPkt) -> error::Result,
+ {
+ let info = &mut *(user as *mut ListenHandler);
+
+ let data = slice::from_raw_parts(data, (*header).caplen as usize);
+
+ if data.len() < 14 {
+ eprintln!(
+ " * Failed to get full packet, captured {} bytes",
+ data.len()
+ );
+ }
+
+ let result = (info.packet_handler)(info.interface, packets::EthernetPkt { data });
+
+ match result {
+ Err(e) => {
+ eprintln!(" * Packet handle error: {:?}", e);
+ if info.break_on_fail {
+ info.fail_error = Some(e);
+ ffi::pcap_breakloop(info.interface.dev);
+ }
+ }
+ Ok(b) => {
+ if b {
+ ffi::pcap_breakloop(info.interface.dev);
+ }
+ }
+ }
+ }
+
+ let interface = Interface {
+ dev_name: self.dev_name.clone(),
+ dev: self.dev,
+ marker: std::marker::PhantomData,
+ absorbed: true,
+ nonblocking: self.nonblocking,
+ };
+
+ let mut info = ListenHandler:: {
+ packet_handler,
+ break_on_fail,
+ fail_error: None,
+ interface: &interface,
+ };
+
+ let count = unsafe {
+ ffi::pcap_loop(
+ self.dev,
+ packet_count,
+ cback::,
+ &mut info as *mut ListenHandler as *mut libc::c_void,
+ )
+ };
+
+ (info.fail_error, count)
+ }
+
+ #[cfg(target_os = "linux")]
+ pub fn stream(mut self) -> error::Result> {
+ self.set_non_blocking(true)?;
+
+ Ok(stream::InterfaceStream {
+ inner: tokio::io::unix::AsyncFd::with_interest(
+ stream::InternalInterfaceStream::::new(unsafe { std::mem::transmute(self) })?,
+ tokio::io::Interest::READABLE,
+ )?,
+ })
+ }
+}
diff --git a/pcap-sys/src/stream.rs b/pcap-sys/src/stream.rs
new file mode 100644
index 0000000..2554161
--- /dev/null
+++ b/pcap-sys/src/stream.rs
@@ -0,0 +1,340 @@
+// Copyright (C) 2023 Andrew Rioux
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+use std::{
+ collections::HashMap,
+ os::fd::{AsRawFd, RawFd},
+ pin::Pin,
+ task::{self, Poll},
+};
+
+use futures::{ready, StreamExt};
+use tokio::io::unix::AsyncFd;
+use tokio_stream::StreamMap;
+
+use super::{error, ffi, packets, Activated, NotListening, Disabled, State, Interface, DevActivated, DevDisabled, PcapDevIterator};
+
+pub(crate) struct InternalInterfaceStream {
+ interface: Interface,
+ fd: RawFd,
+}
+
+impl InternalInterfaceStream {
+ pub(crate) fn new(interface: Interface) -> error::Result> {
+ let fd = unsafe { ffi::pcap_get_selectable_fd(interface.dev) };
+ if fd == -1 {
+ return Err(error::Error::InvalidPcapFd);
+ }
+ Ok(Self { interface, fd })
+ }
+}
+
+impl AsRawFd for InternalInterfaceStream {
+ fn as_raw_fd(&self) -> RawFd {
+ self.fd
+ }
+}
+
+pub struct InterfaceStream {
+ pub(crate) inner: AsyncFd>,
+}
+
+impl InterfaceStream {
+ pub fn sendpacket(&mut self, packet: packets::EthernetPkt) -> error::Result<()> {
+ self.inner.get_mut().interface.sendpacket(packet)
+ }
+
+ pub fn set_filter(
+ &mut self,
+ filter: &str,
+ optimize: bool,
+ mask: Option,
+ ) -> error::Result> {
+ self.inner
+ .get_mut()
+ .interface
+ .set_filter(filter, optimize, mask)
+ }
+}
+
+impl Unpin for InterfaceStream {}
+
+impl futures::Stream for InterfaceStream {
+ type Item = error::Result;
+
+ fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context) -> Poll