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> { + let stream = Pin::into_inner(self); + + loop { + let mut guard = ready!(stream.inner.poll_read_ready_mut(cx))?; + + match guard.try_io(|inner| match inner.get_mut().interface.next_packet() { + Ok(p) => Ok(Ok(p)), + Err(e) => Ok(Err(e)), + }) { + Ok(result) => { + return Poll::Ready(Some(result?)); + } + Err(_would_block) => continue, + } + } + } +} + +pub fn new_aggregate_interface_filtered( + crash: bool, + mut f: F, +) -> error::Result> +where + F: FnMut(&str) -> bool, +{ + let interfaces = if crash { + PcapDevIterator::new()? + .filter(|s| (f)(s)) + .map(|if_name| { + let new_name = if_name.clone(); + Interface::::new(&if_name) + .map(|interface| (if_name, interface)) + .map_err(|e| e.add_ifname(&new_name)) + }) + .collect::>>()? + } else { + PcapDevIterator::new()? + .filter(|s| (f)(s)) + .filter_map(|if_name| { + let new_name = if_name.clone(); + Interface::::new(&if_name) + .map(|interface| (if_name, interface)) + .ok() + .or_else(|| { + println!("{} failed to create device", new_name); + None + }) + }) + .collect::>() + }; + + Ok(AggregateInterface { interfaces, crash }) +} + +pub fn new_aggregate_interface(crash: bool) -> error::Result> { + new_aggregate_interface_filtered(crash, |_| true) +} + +pub struct AggregateInterface { + interfaces: HashMap>, + crash: bool, +} + +impl AggregateInterface { + pub fn set_non_blocking(&mut self, nonblocking: bool) -> error::Result<()> { + for (n, i) in self.interfaces.iter_mut() { + i.set_non_blocking(nonblocking) + .map_err(|e| e.add_ifname(n))?; + } + + Ok(()) + } + + pub fn lookupnets(&self) -> error::Result> { + self.interfaces + .iter() + .map(|(name, interface)| { + interface + .lookupnet() + .map(|net| (&**name, net)) + .map_err(|e| e.add_ifname(&name)) + }) + .collect::>() + } + + pub fn get_ifnames(&self) -> Vec<&str> { + self.interfaces.keys().map(|n| &**n).collect::<_>() + } +} + +impl AggregateInterface { + pub fn set_promisc(&mut self, promisc: bool) -> error::Result<()> { + for (n, i) in self.interfaces.iter_mut() { + i.set_promisc(promisc).map_err(|e| e.add_ifname(n))?; + } + + Ok(()) + } + + pub fn set_buffer_size(&mut self, bufsize: i32) -> error::Result<()> { + for (n, i) in self.interfaces.iter_mut() { + i.set_buffer_size(bufsize).map_err(|e| e.add_ifname(n))?; + } + + Ok(()) + } + + pub fn set_timeout(&mut self, timeout: i32) -> error::Result<()> { + for (n, i) in self.interfaces.iter_mut() { + i.set_timeout(timeout).map_err(|e| e.add_ifname(n))?; + } + + Ok(()) + } + + pub fn activate(self) -> error::Result> { + Ok(AggregateInterface { + interfaces: if self.crash { + self.interfaces + .into_iter() + .map(|(name, interface)| { + let new_name = name.clone(); + interface + .activate() + .map(|interface| (name, interface)) + .map_err(|e| e.add_ifname(&new_name)) + }) + .collect::>()? + } else { + self.interfaces + .into_iter() + .filter_map(|(name, interface)| { + let name_clone = name.clone(); + interface + .activate() + .map(|interface| (name, interface)) + .ok() + .or_else(|| { + println!("{} failed to activate", name_clone); + None + }) + }) + .collect::<_>() + }, + crash: self.crash, + }) + } +} + +impl AggregateInterface { + pub fn datalinks(&self) -> HashMap<&str, i32> { + self.interfaces + .iter() + .map(|(name, interface)| (&**name, interface.datalink())) + .collect::<_>() + } + + pub fn prune(&mut self, mut f: F) + where + F: FnMut(&str, &mut Interface) -> bool, + { + let to_prune = self + .interfaces + .iter_mut() + .filter_map(|(k, v)| if (f)(k, v) { Some(k.clone()) } else { None }) + .collect::>(); + + for name in to_prune { + self.interfaces.remove(&name); + } + } + + pub fn set_filter( + &mut self, + filter: &str, + optimize: bool, + mask: Option, + ) -> error::Result>> { + if self.crash { + self.interfaces + .iter_mut() + .map(|(name, interface)| { + interface + .set_filter(filter, optimize, mask) + .map(|bpf| (&**name, bpf)) + .map_err(|e| e.add_ifname(&name)) + }) + .collect::>() + } else { + Ok(self + .interfaces + .iter_mut() + .filter_map(|(name, interface)| { + let name_clone = name.clone(); + interface + .set_filter(filter, optimize, mask) + .map(|bpf| (&**name, bpf)) + .ok() + .or_else(|| { + println!("{} failed to set filter", name_clone); + None + }) + }) + .collect::<_>()) + } + } + + pub fn sendpacket(&self, ifname: &str, packet: packets::EthernetPkt) -> error::Result<()> { + if let Some(interface) = self.interfaces.get(ifname) { + interface + .sendpacket(packet) + .map_err(|e| e.add_ifname(ifname))?; + } + + Ok(()) + } +} + +impl AggregateInterface { + pub fn stream(self) -> error::Result> { + Ok(AggregateInterfaceStream { + streams: self + .interfaces + .into_iter() + .map(|(ifname, interface)| { + let new_name = ifname.clone(); + interface + .stream() + .map(|stream| (ifname, stream)) + .map_err(|e| e.add_ifname(&new_name)) + }) + .collect::>()?, + }) + } +} + +pub struct AggregateInterfaceStream { + streams: StreamMap>, +} + +impl AggregateInterfaceStream { + pub fn get_ifnames(&self) -> Vec<&str> { + self.streams.keys().map(|n| &**n).collect::<_>() + } + + pub fn sendpacket(&mut self, ifname: &str, packet: packets::EthernetPkt) -> error::Result<()> { + if let Some(interface) = self.streams.values_mut().find(|interface| { + interface.inner.get_ref().interface.dev_name.as_bytes() == ifname.as_bytes() + }) { + interface.sendpacket(packet)?; + } + + Ok(()) + } +} + +impl futures::Stream for AggregateInterfaceStream { + type Item = (String, error::Result); + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + self.streams.poll_next_unpin(cx) + } +} diff --git a/sparse-beacon/Cargo.toml b/sparse-beacon/Cargo.toml index 82cefbb..c963ecf 100644 --- a/sparse-beacon/Cargo.toml +++ b/sparse-beacon/Cargo.toml @@ -2,3 +2,6 @@ name = "sparse-beacon" version = "0.7.0" edition = "2021" + +[dependencies] +nl-sys = { path = "../nl-sys" } diff --git a/sparse-beacon/src/main.rs b/sparse-beacon/src/main.rs index ac6bb53..8cad6a0 100644 --- a/sparse-beacon/src/main.rs +++ b/sparse-beacon/src/main.rs @@ -1,3 +1,16 @@ +use nl_sys::{netlink, route}; + fn main() { - println!("hi there"); + let ip = std::net::Ipv4Addr::new(192, 168, 3, 10); + + let (ifname, _, srcip, src_mac, dst_mac, _) = { + let socket = netlink::Socket::new().unwrap(); + + let routes = socket.get_routes().unwrap(); + let neighs = socket.get_neigh().unwrap(); + let links = socket.get_links().unwrap(); + let addrs = socket.get_addrs().unwrap(); + + route::get_macs_and_src_for_ip(&addrs, &routes, &neighs, &links, ip).unwrap() + }; } diff --git a/system-libs.nix b/system-libs.nix index 1ebab9a..d127dc1 100644 --- a/system-libs.nix +++ b/system-libs.nix @@ -1,4 +1,4 @@ -{ pkgs, libpcap-src, winpcap-installer, freebsd-libs-packed }: +{ pkgs, libnl-src, libpcap-src, winpcap-installer, freebsd-libs-packed }: let freebsd-libs = pkgs.stdenv.mkDerivation { name = "freebsd-libs"; @@ -44,31 +44,64 @@ in { libnl = pkgs.stdenv.mkDerivation { name = "libnl-static"; - buildInputs = with pkgs; [ autoconf automake libtool ]; + nativeBuildInputs = with pkgs; [ + musl + clang + autoconf + automake + libtool + pkg-config + autoconf-archive + flex + bison + ]; + src = libnl-src; + + configurePhase = '' + export CC=musl-clang + export CXX=musl-clang + aclocal + autoreconf -vi + ./configure \ + --enable-static \ + --disable-shared + ''; + + buildPhase = '' + export CC=musl-clang + export CXX=musl-clang + make + ''; + + installPhase = '' + mkdir -p $out/lib + cp lib/.libs/*.a $out/lib + ''; }; libpcap-linux = pkgs.stdenv.mkDerivation { name = "libpcap-static-linux"; - buildInputs = with pkgs; [ automake bison cmake flex ]; + buildInputs = with pkgs; [ clang automake bison cmake flex musl ]; src = libpcap-src; configurePhase = '' + export CC=musl-clang cmake \ - -DCMAKE_BUILD_TYPE=MinSizeRel \ - -DBUILD_SHARED_LIBS=OFF \ - -DDISABLE_BLUETOOTH=ON \ - -DDISABLE_DAG=ON \ - -DDISABLE_DBUS=ON \ - -DDISABLE_DPDK=ON \ - -DDISABLE_NETMAP=ON \ - -DDISABLE_RDMA=ON \ - -DDISABLE_LINUX_USBMON=ON \ - -DDISABLE_SNF=ON \ - -DPCAP_TYPE=linux \ - . + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DBUILD_SHARED_LIBS=OFF \ + -DDISABLE_BLUETOOTH=ON \ + -DDISABLE_DAG=ON \ + -DDISABLE_DBUS=ON \ + -DDISABLE_DPDK=ON \ + -DDISABLE_NETMAP=ON \ + -DDISABLE_RDMA=ON \ + -DDISABLE_LINUX_USBMON=ON \ + -DDISABLE_SNF=ON \ + -DPCAP_TYPE=linux \ + . ''; buildPhase = '' @@ -97,19 +130,21 @@ in { src = libpcap-src; configurePhase = '' + export CC=x86_64-unknown-freebsd-clang + export CXX=x86_64-unknown-freebsd-clang++ cmake \ - -DCMAKE_TOOLCHAIN_FILE=${bsd-toolchain} \ - -DCMAKE_BUILD_TYPE=MinSizeRel \ - -DBUILD_SHARED_LIBS=OFF \ - -DDISABLE_BLUETOOTH=ON \ - -DDISABLE_DAG=ON \ - -DDISABLE_DBUS=ON \ - -DDISABLE_DPDK=ON \ - -DDISABLE_NETMAP=ON \ - -DDISABLE_RDMA=ON \ - -DDISABLE_SNF=ON \ - -DPCAP_TYPE=bpf \ - . + -DCMAKE_TOOLCHAIN_FILE=${bsd-toolchain} \ + -DCMAKE_BUILD_TYPE=MinSizeRel \ + -DBUILD_SHARED_LIBS=OFF \ + -DDISABLE_BLUETOOTH=ON \ + -DDISABLE_DAG=ON \ + -DDISABLE_DBUS=ON \ + -DDISABLE_DPDK=ON \ + -DDISABLE_NETMAP=ON \ + -DDISABLE_RDMA=ON \ + -DDISABLE_SNF=ON \ + -DPCAP_TYPE=bpf \ + . ''; buildPhase = ''