diff --git a/.gitignore b/.gitignore index 53eaa21..1a1b907 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target +/builds **/*.rs.bk diff --git a/.semaphore/Dockerfile b/.semaphore/Dockerfile new file mode 100644 index 0000000..4dbc30d --- /dev/null +++ b/.semaphore/Dockerfile @@ -0,0 +1,7 @@ +FROM rust:1.64.0-slim + +# Install SemaphoreCI dependencies +# https://docs.semaphoreci.com/ci-cd-environment/custom-ci-cd-environment-with-docker/ +RUN apt-get update && apt-get install -y curl git openssh-client + +CMD ["/bin/bash"] diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml new file mode 100644 index 0000000..79b3c74 --- /dev/null +++ b/.semaphore/semaphore.yml @@ -0,0 +1,20 @@ +version: v1.0 +name: Rust +agent: + machine: + type: e1-standard-2 + os_image: ubuntu2004 + containers: + # Rust 1.64 (2021 edition) is currently not supported in Semaphore + - name: main + image: 'saluki/rust-ci:1.64' +blocks: + - name: Test release + task: + jobs: + - name: Build & test + commands: + - checkout + - rustc --version + - cargo build --verbose + - cargo test --verbose diff --git a/Cargo.lock b/Cargo.lock index 81ff51d..5617d4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,53 +1,110 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] [[package]] name = "ansi_term" -version = "0.11.0" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e579a7752471abc2a8268df8b20005e3eadd975f585398f17efcfd8d4927371" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +checksum = "4bcd8291a340dd8ac70e18878bc4501dd7b4ff970cfa21c207d36ece51ea88fd" dependencies = [ - "winapi 0.3.9", + "anstyle", + "windows-sys 0.48.0", ] [[package]] name = "arp-scan" -version = "0.1.0" +version = "0.13.1" dependencies = [ + "ansi_term", "clap", + "csv", + "ctrlc", + "dns-lookup", + "ipnetwork", "pnet", + "pnet_datalink", + "rand", + "serde", + "serde_json", + "serde_yaml", + "sudo", ] [[package]] -name = "atty" -version = "0.2.14" +name = "autocfg" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi 0.3.9", -] +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" -version = "0.5.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f67931368edf3a9a51d29886d245f1c3db2f1ef0dcc9e35ff70341b78c10d23" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "bitflags" -version = "1.2.1" +name = "cc" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" [[package]] name = "cfg-if" @@ -57,88 +114,229 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "2.33.3" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "956ac1f6381d8d82ab4684768f89c0ea3afe66925ceadb4eeb3fc452ffc55d62" dependencies = [ - "ansi_term", - "atty", - "bitflags 1.2.1", + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84080e799e54cff944f4b4a4b0e71630b0e0443b25b985175c7dddc1a859b749" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", "strsim", - "textwrap", - "unicode-width", - "vec_map", +] + +[[package]] +name = "clap_lex" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "csv" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctrlc" +version = "3.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" +dependencies = [ + "nix", + "windows-sys 0.45.0", +] + +[[package]] +name = "dns-lookup" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53ecafc952c4528d9b51a458d1a8904b81783feff9fde08ab6ed2545ff396872" +dependencies = [ + "cfg-if", + "libc", + "socket2", + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "getrandom" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +dependencies = [ + "cfg-if", + "libc", + "wasi", ] [[package]] name = "glob" -version = "0.2.11" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ + "hermit-abi", "libc", + "windows-sys 0.48.0", ] [[package]] name = "ipnetwork" -version = "0.17.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c3eaab3ac0ede60ffa41add21970a7df7d91772c03383aac6c2c3d53cc716b" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" dependencies = [ "serde", ] [[package]] -name = "kernel32-sys" -version = "0.2.2" +name = "is-terminal" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", ] +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + [[package]] name = "libc" -version = "0.2.93" +version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" + +[[package]] +name = "linux-raw-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b085a4f2cde5781fc4b1717f2e86c62f5cda49de7ba99a7c2eae02b61c9064c" [[package]] name = "log" -version = "0.3.9" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e19e8d5c34a3e0e2223db8e060f9e8264aeeb5c5fc64a4ee9965c062211c024b" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ - "log 0.4.14", + "cfg-if", ] [[package]] -name = "log" -version = "0.4.14" +name = "memchr" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" dependencies = [ + "bitflags", "cfg-if", + "libc", + "static_assertions", ] [[package]] -name = "memchr" -version = "2.4.0" +name = "no-std-net" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" [[package]] name = "pnet" -version = "0.27.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b657d5b9a98a2c81b82549922b8b15984e49f8120cd130b11a09f81b9b55d633" +checksum = "cd959a8268165518e2bf5546ba84c7b3222744435616381df3c456fe8d983576" dependencies = [ "ipnetwork", "pnet_base", @@ -150,71 +348,74 @@ dependencies = [ [[package]] name = "pnet_base" -version = "0.27.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4688aa497ef62129f302a5800ebde67825f8ff129f43690ca84099f6620bed" +checksum = "872e46346144ebf35219ccaa64b1dffacd9c6f188cd7d012bd6977a2a838f42e" +dependencies = [ + "no-std-net", +] [[package]] name = "pnet_datalink" -version = "0.27.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59001c9c4d9d23bf2f61afaaf134a766fd6932ba2557c606b9112157053b9ac7" +checksum = "c302da22118d2793c312a35fb3da6846cb0fab6c3ad53fd67e37809b06cdafce" dependencies = [ "ipnetwork", "libc", "pnet_base", "pnet_sys", - "winapi 0.3.9", + "winapi", ] [[package]] name = "pnet_macros" -version = "0.27.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d894a90dbdbe976e624453fc31b1912f658083778329442dda1cca94f76a3e76" +checksum = "2a780e80005c2e463ec25a6e9f928630049a10b43945fea83207207d4a7606f4" dependencies = [ + "proc-macro2", + "quote", "regex", - "syntex", - "syntex_syntax", + "syn 1.0.109", ] [[package]] name = "pnet_macros_support" -version = "0.27.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b99269a458570bc06a9132254349f6543d9abc92e88b68d8de934aac9481f6c" +checksum = "e6d932134f32efd7834eb8b16d42418dac87086347d1bc7d142370ef078582bc" dependencies = [ "pnet_base", ] [[package]] name = "pnet_packet" -version = "0.27.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f8238f4eb897a55ca06510cd71afb5b5ca7b4ff2d7188f1ca855fc1710133e" +checksum = "8bde678bbd85cb1c2d99dc9fc596e57f03aa725f84f3168b0eaf33eeccb41706" dependencies = [ "glob", "pnet_base", "pnet_macros", "pnet_macros_support", - "syntex", ] [[package]] name = "pnet_sys" -version = "0.27.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7589e4c4e7ed72a3ffdff8a65d3bea84e8c3a23e19d0a10e8f45efdf632fff15" +checksum = "faf7a58b2803d818a374be9278a1fe8f88fce14b936afbe225000cfcd9c73f16" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] name = "pnet_transport" -version = "0.27.2" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "326abdfd2e70e8e943bd58087b59686de170cac050a3b19c9fcc84db01690af5" +checksum = "813d1c0e4defbe7ee22f6fe1755f122b77bfb5abe77145b1b5baaf463cab9249" dependencies = [ "libc", "pnet_base", @@ -222,11 +423,65 @@ dependencies = [ "pnet_sys", ] +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "regex" -version = "1.5.3" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -235,119 +490,151 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] -name = "rustc-serialize" -version = "0.3.24" +name = "rustix" +version = "0.37.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" +checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "serde" -version = "1.0.125" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] [[package]] -name = "strsim" -version = "0.8.0" +name = "serde_derive" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] [[package]] -name = "syntex" -version = "0.42.2" +name = "serde_json" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a30b08a6b383a22e5f6edc127d169670d48f905bb00ca79a00ea3e442ebe317" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ - "syntex_errors", - "syntex_syntax", + "itoa", + "ryu", + "serde", ] [[package]] -name = "syntex_errors" -version = "0.42.0" +name = "serde_yaml" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04c48f32867b6114449155b2a82114b86d4b09e1bddb21c47ff104ab9172b646" +checksum = "d9d684e3ec7de3bf5466b32bd75303ac16f0736426e5a4e0d6e489559ce1249c" dependencies = [ - "libc", - "log 0.3.9", - "rustc-serialize", - "syntex_pos", - "term", - "unicode-xid", + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", ] [[package]] -name = "syntex_pos" -version = "0.42.0" +name = "socket2" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd49988e52451813c61fecbe9abb5cfd4e1b7bb6cdbb980a6fbcbab859171a6" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ - "rustc-serialize", + "libc", + "winapi", ] [[package]] -name = "syntex_syntax" -version = "0.42.0" +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7628a0506e8f9666fdabb5f265d0059b059edac9a3f810bda077abb5d826bd8d" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "sudo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bd84d4c082e18e37fef52c0088e4407dabcef19d23a607fb4b5ee03b7d5b83" dependencies = [ - "bitflags 0.5.0", "libc", - "log 0.3.9", - "rustc-serialize", - "syntex_errors", - "syntex_pos", - "term", - "unicode-xid", + "log", ] [[package]] -name = "term" -version = "0.4.6" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa63644f74ce96fbeb9b794f66aff2a52d601cbd5e80f4b97123e3899f4570f1" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "kernel32-sys", - "winapi 0.2.8", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "syn" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ - "unicode-width", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "unicode-width" -version = "0.1.8" +name = "unicode-ident" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] -name = "unicode-xid" -version = "0.0.3" +name = "unsafe-libyaml" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36dff09cafb4ec7c8cf0023eb0b686cb6ce65499116a12201c9e11840ca01beb" +checksum = "1865806a559042e51ab5414598446a5871b561d21b6764f2eabb0dd481d880a6" [[package]] -name = "vec_map" -version = "0.8.2" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] -name = "winapi" -version = "0.2.8" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "winapi" @@ -359,12 +646,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -376,3 +657,135 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml index ae7551a..337325f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,35 @@ [package] name = "arp-scan" +description = "A minimalistic ARP scan tool" license = "AGPL-3.0-or-later" -version = "0.1.0" +version = "0.13.1" authors = ["Saluki"] -edition = "2018" +edition = "2021" +readme = "README.md" +homepage = "https://github.com/Saluki/arp-scan-rs" +repository = "https://github.com/Saluki/arp-scan-rs" +keywords = ["arp", "scan", "network", "security"] +categories = ["command-line-utilities"] +exclude = ["/.semaphore", "/data", "/release.sh", ".*"] +rust-version = "1.64" [dependencies] -pnet = "0.27.2" -clap = "2.33.3" + +# CLI & utilities +clap = { version = "4.2", default-features = false, features = ["std", "suggestions", "color", "help"] } +ansi_term = "0.12" +rand = "0.8" +ctrlc = "3.2" + +# Network +pnet = "0.33" +pnet_datalink = "0.33" +ipnetwork = "0.20" +dns-lookup = "1.0" + +# Parsing & exports +csv = "1.1" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.9" +sudo = "0.6" diff --git a/README.md b/README.md index c324985..41412a3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,229 @@ # ARP scanner CLI [![Build Status](https://saluki.semaphoreci.com/badges/arp-scan-rs/branches/master.svg?style=shields)](https://saluki.semaphoreci.com/projects/arp-scan-rs) +[![dependency status](https://deps.rs/repo/github/Saluki/arp-scan-rs/status.svg)](https://deps.rs/repo/github/Saluki/arp-scan-rs) +![crates.io](https://img.shields.io/crates/v/arp-scan.svg) Find all hosts in your local network using this fast ARP scanner. The CLI is written in Rust and provides a minimal scanner that finds all hosts using the ARP protocol. Inspired by the awesome [arp-scan project](https://github.com/royhills/arp-scan). + +✔ Minimal Rust binary & fast ARP scans + +✔ Scan customization (ARP, timings, interface, DNS, ...) + +✔ MAC vendor search + +✔ JSON, YAML & CSV exports + +✔ Pre-defined scan profiles (default, fast, stealth & chaos) + +## Examples + +Start by listing all network interfaces on the host. + +```bash +# List all network interfaces +$ arp-scan -l + +lo ✔ UP 00:00:00:00:00:00 127.0.0.1/8 +enp3s0f0 ✔ UP 4f:6e:cd:78:bb:5a +enp4s0 ✖ DOWN d0:c5:e9:40:00:4a +wlp1s0 ✔ UP d2:71:d8:29:a8:72 192.168.1.21/24 +docker0 ✔ UP 49:fd:cd:60:73:77 172.17.0.1/16 +br-fa6dc54a91ee ✔ UP 61:ab:c1:a7:50:79 172.18.0.1/16 + +Found 6 network interfaces, 5 seems up for ARP scan +Default network interface will be wlp1s0 + +``` + +Perform a default ARP scan on the local network with safe defaults. + +```bash +# Perform a scan on the default network interface +$ arp-scan + +Selected interface wlp1s0 with IP 192.168.1.21/24 +Estimated scan time 2068ms (10752 bytes, 14000 bytes/s) +Sending 256 ARP requests (waiting at least 800ms, 0ms request interval) + +| IPv4 | MAC | Hostname | Vendor | +|-----------------|-------------------|--------------|--------------| +| 192.168.1.1 | 91:10:fb:30:06:04 | router.home | Vendor, Inc. | +| 192.168.1.11 | 45:2e:99:bc:22:b6 | host-a.home | | +| 192.168.1.15 | bc:03:c2:92:47:df | host-b.home | Vendor, Inc. | +| 192.168.1.18 | 8d:eb:56:17:b8:e1 | host-c.home | Vendor, Inc. | +| 192.168.1.34 | 35:e0:6c:1e:e3:fe | | Vendor, Inc. | + +ARP scan finished, 5 hosts found in 1.623 seconds +7 packets received, 5 ARP packets filtered + +``` + +## Getting started + +Download the `arp-scan` binary for Linux (Ubuntu, Fedora, Debian, ...). See the [releases page](https://github.com/Saluki/arp-scan-rs/releases) for other binaries. + +```bash +wget -O arp-scan https://github.com/Saluki/arp-scan-rs/releases/download/v0.13.1/arp-scan-v0.13.1-x86_64-unknown-linux-musl && chmod +x ./arp-scan +``` + +Optionnaly, fetch the IEEE OUI reference file (CSV format) that contains all MAC address vendors. + +```bash +wget -O /usr/share/arp-scan/ieee-oui.csv http://standards-oui.ieee.org/oui/oui.csv +``` + +List all available network interfaces. + +```bash +./arp-scan -l +``` + +Launch a scan on interface `wlp1s0`. + +```bash +./arp-scan -i wlp1s0 +``` + +Enhance the minimum scan timeout to 5 seconds (by default, 2 seconds). + +```bash +./arp-scan -i wlp1s0 -t 5s +``` + +Perform an ARP scan on the default network interface, VLAN 45 and JSON output. + +```bash +./arp-scan -Q 45 -o json +``` + +## Options + +#### Get help `-h` + +Display the main help message with all commands and available ARP scan options. + +#### List interfaces `-l` + +List all available network interfaces. Using this option will only print a list of interfaces and exit the process. + +#### Select scan profile `-p stealth` + +A scan profile groups together a set of ARP scan options to perform a specific scan. The scan profiles are listed below: + +- `default` : default option, this is enabled if the `-p` option is not used +- `fast` : fast ARP scans, the results may be less accurate +- `stealth` : slower scans that minimize the network impact +- `chaos` : randomly-selected values for the ARP scan + +#### Select interface `-i eth0` + +Perform a scan on the network interface `eth0`. The first valid IPv4 network on this interface will be used as scan target. By default, the first network interface with an `up` status and a valid IPv4 will be selected. + +#### Set IPv4 network range `-n 172.17.0.0/24` + +By default, the scan process will select the first IPv4 network on the interface and start a scan on the whole range. With the `--network` option, an IPv4 network can be defined _(this may be used for specific scans on a subset of network targets)_. + +#### Set global scan timeout `-t 15s` + +Enforce a timeout of at least 15 seconds. This timeout is a minimum value (scans may take a little more time). Default value is `2000ms`. + +#### Change ARP request interval `-I 39ms` + +By default, a `10ms` gap will be set between ARP requests to avoid an ARP storm on the network. This value can be changed to reduce or increase the milliseconds between each ARP request. + +#### Enforce scan bandwidth limit `-B 1000` + +Enforce a bandwidth limit (expressed in bits per second) on ARP scans. The `--bandwidth` option conflicts with `--interval` since these 2 arguments change the same parameter underneath. + +#### Numeric mode `--numeric` + +Switch to numeric mode. This will skip the local hostname resolution process and will only display IP addresses. + +#### Host retry count `-r 3` + +Send 3 ARP requests to the targets (retry count). By default, a single ARP request will be sent to each host. + +#### Change source IPv4 `-S 192.168.1.130` + +Change or force the IPv4 address sent as source in the broadcasted ARP packets. By default, a valid IPv4 address on the network interface will be used. This option may be useful for isolated hosts and security checks. + +#### Change destination MAC `-M 55:44:33:22:11:00` + +Change or force the MAC address sent as destination ARP request. By default, a broadcast destination (`00:00:00:00:00:00`) will be set. + +#### Change source MAC `-M 11:24:71:29:21:76` + +Change or force the MAC address sent as source in the ARP request. By default, the network interface MAC will be used. + +#### Randomize target list `-R` + +Randomize the IPv4 target list before sending ARP requests. By default, all ARP requests are sent in ascending order by IPv4 address. + +#### Use custom MAC OUI file `--oui-file ./my-file.csv` + +Use a [custom OUI MAC file](http://standards-oui.ieee.org/oui/oui.csv), the default path will be set to `/usr/share/arp-scan/ieee-oui.csv"`. + +#### Set VLAN ID `-Q 42` + +Add a 802.1Q field in the Ethernet frame. This fields contains the given VLAN ID for outgoing ARP requests. By default, the Ethernet frame is sent without 802.1Q fields (no VLAN). + +#### Customize ARP operation ID `--arp-op 1` + +Change the ARP protocol operation field, this can cause scan failure. + +#### Customize ARP hardware type `--hw-type 1` + +Change the ARP hardware type field, this can cause scan failure. + +#### Customize ARP hardware address length `--hw-addr 6` + +Change the ARP hardware address length field, this can cause scan failure. + +#### Customize ARP protocol type `--proto-type 2048` + +Change the ARP protocol type field, this can cause scan failure. + +#### Customize ARP protocol adress length `--proto-addr 4` + +Change the ARP protocol address length field, this can cause scan failure. + +#### Set output format `-o json` + +Set the output format to either `plain` (a full-text output with tables), `json`, `yaml` or `csv`. + +#### Show version `--version` + +Display the ARP scan CLI version and exits the process. + +## Roadmap & features + +The features below will be shipped in the next releases of the project. + +- Make ARP scans faster + - with a per-host retry approach + - add a back-off factor for retries + - ~~by closing the response thread faster~~ - released in 0.8.0 +- ~~Scan profiles (standard, attacker, light, ...)~~ - released in 0.10.0 +- Complete VLAN support +- ~~Exports (JSON & YAML)~~ - released in 0.7.0 +- ~~Full ARP packet customization (Ethernet protocol, ARP operation, ...)~~ - released in 0.10.0 +- ~~Time estimations & bandwidth~~ - released in 0.10.0 +- ~~MAC vendor lookup in the results~~ - released in 0.9.0 +- ~~Fine-grained scan timings (interval)~~ - released in 0.8.0 +- ~~Wide network range support~~ - released in 0.13.0 +- ~~Partial results on SIGINT~~ - released in 0.11.0 +- ~~Read network targets from file~~ - released in 0.12.0 +- Adding advanced packet options (padding, LLC, ...) + - add padding bits after ARP payload + - support RFC 1042 LLC framing with SNAP +- ~~Enable bandwith control (exclusive with interval)~~ - released in 0.12.0 +- Stronger profile defaults (chaos & stealth) +- Other platforms (Windows, ...) +- Read targets from *stdout* +- Change verbose options (for debug, network details, quiet mode, ...) +- Avoid packet copy in userspace for faster scans (BPF filtering) + +## Contributing + +Feel free to suggest an improvement, report a bug, or ask something: https://github.com/saluki/arp-scan-rs/issues diff --git a/data/ieee-oui.csv b/data/ieee-oui.csv new file mode 100644 index 0000000..bb5d762 --- /dev/null +++ b/data/ieee-oui.csv @@ -0,0 +1,33 @@ +Registry,Assignment,Organization Name,Organization Address +MA-L,002272,American Micro-Fuel Device Corp.,2181 Buchanan Loop Ferndale WA US 98248 +MA-L,00D0EF,IGT,9295 PROTOTYPE DRIVE RENO NV US 89511 +MA-L,086195,Rockwell Automation,1 Allen-Bradley Dr. Mayfield Heights OH US 44124-6118 +MA-L,F4BD9E,"Cisco Systems, Inc",80 West Tasman Drive San Jose CA US 94568 +MA-L,5885E9,Realme Chongqing MobileTelecommunications Corp Ltd,"No.24 Nichang Boulevard, Huixing Block, Yubei District, Chongqing. Chongqing China CN 401120 " +MA-L,BC2392,BYD Precision Manufacture Company Ltd.,"No.3001, Bao He Road, Baolong Industrial, Longgang Street,Longgang Zone, Shenzhen shenzhen CN 518116 " +MA-L,405582,Nokia,600 March Road Kanata Ontario CA K2K 2E6 +MA-L,A4E31B,Nokia,600 March Road Kanata Ontario CA K2K 2E6 +MA-L,D48660,Arcadyan Corporation,"No.8, Sec.2, Guangfu Rd. Hsinchu City Hsinchu TW 30071 " +MA-L,C489ED,Solid Optics EU N.V.,De Huchtstraat 35 Almere Flevoland NL 1327 EC +MA-L,60F43A,Edifier International,"Suit 2207, 22nd floor, Tower II, Lippo centre, 89 Queensway Hong Kong CN 070 " +MA-L,58A87B,"Fitbit, Inc.","199 Fremont Street, 14th Fl San Francisco CA US 94105 " +MA-L,5C6BD7,Foshan VIOMI Electric Appliance Technology Co. Ltd.,"No.2 North Xinxi Fourth Road, Xiashi Village Committee,Lunjiao Sub-district Office, Shunde District Foshan Guandong CN 528308 " +MA-L,1848CA,"Murata Manufacturing Co., Ltd.","1-10-1, Higashikotari Nagaokakyo-shi Kyoto JP 617-8555 " +MA-L,90EEC7,"Samsung Electronics Co.,Ltd","#94-1, Imsoo-Dong Gumi Gyeongbuk KR 730-350 " +MA-L,1029AB,"Samsung Electronics Co.,Ltd","#94-1, Imsoo-Dong Gumi Gyeongbuk KR 730-350 " +MA-L,184ECB,"Samsung Electronics Co.,Ltd","#94-1, Imsoo-Dong Gumi Gyeongbuk KR 730-350 " +MA-L,010101,SomeCorp,Unknown address +MA-L,8022A7,"NEC Platforms, Ltd.",2-3 Kandatsukasamachi Chiyodaku Tokyo JP 101-8532 +MA-L,B83BCC,Xiaomi Communications Co Ltd,"#019, 9th Floor, Building 6, 33 Xi'erqi Middle Road Beijing Haidian District CN 100085 " +MA-L,88D199,"Vencer Co., Ltd.","14F-12, No. 79, Section 1, Hsin Tai Wu Road, Hsi-Chih District, New Taipei City Taiwan TW 22101 " +MA-L,CCE236,Hangzhou Yaguan Technology Co. LTD,"33rd Floor, T4 US Center, European and American Financial City, Yuhang District, Hangzhou, Zhejiang Hangzhou Zhejiang CN 311100 " +MA-L,204181,ESYSE GmbH Embedded Systems Engineering,Ruth-Niehaus Str. 8 Meerbusch Nordrhein-Westfalen DE 40667 +MA-L,DCBB96,Full Solution Telecom,"Calle 130A #59C-42, Barrio Ciudad Jardin Norte Bogota Distrito Capital de Bogota CO 111111 " +MA-L,74765B,"Quectel Wireless Solutions Co.,Ltd.","7th Floor, Hongye Building, No.1801 Hongmei Road, Xuhui District Shanghai CN 200233 " +MA-L,B437D8,D-Link (Shanghai) Limited Corp.,"Room 612, Floor 6, No.88, Taigu Road, Shanghai CN 200131 " +MA-L,9CD57D,"Cisco Systems, Inc",80 West Tasman Drive San Jose CA US 94568 +MA-L,941F3A,Ambiq,"6500 River Place Blvd., Building 7, Suite 200 Austin TX US 78730 " +MA-L,2C3557,"ELIIY Power CO., Ltd.","1-6-4, Osaki Shinagawa-ku TOKYO US 141-0032 " +MA-L,7066E1,dnt Innovation GmbH,Maiburger Straße 29 Leer DE 26789 +MA-L,F8CE72,Wistron Corporation," NO.5, HSIN AN ROAD, SCIENCE-BASED INDUSTRIAL PARK, HSINCHU, TAIWAN, R.O.C. Hsinchu County Taiwan TW 303036 " +MA-L,CC9DA2,Eltex Enterprise Ltd.,Okruzhnaya st. 29v Novosibirsk RU 630020 diff --git a/data/ip-list.txt b/data/ip-list.txt new file mode 100644 index 0000000..4dc0850 --- /dev/null +++ b/data/ip-list.txt @@ -0,0 +1,3 @@ +192.168.1.1 +192.168.1.2 +192.168.2.0/29 diff --git a/release.sh b/release.sh new file mode 100755 index 0000000..a072ab5 --- /dev/null +++ b/release.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# RELEASE UTILITY +# This script helps with the release process on Github (musl & glibc builds for Linux) + +mkdir -p ./builds +rm -rf ./builds/* + +CLI_VERSION=$(/usr/bin/cat Cargo.toml | egrep "version = (.*)" | egrep -o --color=never "([0-9]+\.?){3}" | head -n 1) +echo "Releasing v$CLI_VERSION for GNU & musl targets" + +# Build a 'musl' release for Linux x86_64 +cargo build --release --target=x86_64-unknown-linux-musl --locked +cp -p ./target/x86_64-unknown-linux-musl/release/arp-scan ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-musl +./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-musl --version + +# Build a 'glibc' (GNU) release for Linux x86_64 +cargo build --release --target=x86_64-unknown-linux-gnu --locked +cp -p ./target/x86_64-unknown-linux-gnu/release/arp-scan ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc +./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-glibc --version + +# Build the deb archive +mkdir -p ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN +echo "Package: arp-scan-rs" > ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Version: $CLI_VERSION" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Architecture: amd64" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Maintainer: Saluki" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +echo "Description: Minimalist ARP scan written in Rust" >> ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/DEBIAN/control +mkdir -p ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/usr/local/bin +cp ./builds/arp-scan-v$CLI_VERSION-x86_64-unknown-linux-musl ./builds/arp-scan-rs_$CLI_VERSION-1_amd64/usr/local/bin/arp-scan +(cd ./builds && dpkg-deb --build --root-owner-group arp-scan-rs_$CLI_VERSION-1_amd64) + +echo "Update the README instructions for v$CLI_VERSION" +echo " ✓ Publish on crates.io" +echo " ✓ Release on Github with Git tag v$CLI_VERSION" diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..8f27aa3 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,669 @@ +use std::str::FromStr; +use std::net::Ipv4Addr; +use std::process; +use std::sync::Arc; +use std::path::Path; +use std::fs; + +use clap::{Arg, ArgMatches, Command, ArgAction}; +use ipnetwork::IpNetwork; +use pnet_datalink::MacAddr; +use pnet::packet::arp::{ArpHardwareType, ArpOperation}; +use pnet::packet::ethernet::EtherType; + +use crate::time::parse_to_milliseconds; + +const TIMEOUT_MS_FAST: u64 = 800; +const TIMEOUT_MS_DEFAULT: u64 = 2000; + +const HOST_RETRY_DEFAULT: usize = 1; +const REQUEST_MS_INTERVAL: u64 = 10; + +const CLI_VERSION: &str = env!("CARGO_PKG_VERSION"); + +const EXAMPLES_HELP: &str = "EXAMPLES: + + # Launch a default scan with on the first working interface + arp-scan + + # List network interfaces + arp-scan -l + + # Launch a scan on a specific range + arp-scan -i eth0 -n 10.37.3.1,10.37.4.55/24 + + # Launch a scan on WiFi interface with fake IP and stealth profile + arp-scan -i eth0 --source-ip 192.168.0.42 --profile stealth + + # Launch a scan on VLAN 45 with JSON output + arp-scan -Q 45 -o json + +"; + +/** + * This function groups together all exposed CLI arguments to the end-users + * with clap. Other CLI details (version, ...) should be grouped there as well. + */ +pub fn build_args() -> Command { + + Command::new("arp-scan") + .version(CLI_VERSION) + .about("A minimalistic ARP scan tool written in Rust") + .arg( + Arg::new("profile").short('p').long("profile") + .value_name("PROFILE_NAME") + .help("Scan profile") + ) + .arg( + Arg::new("interface").short('i').long("interface") + .value_name("INTERFACE_NAME") + .help("Network interface") + ) + .arg( + Arg::new("network").short('n').long("network") + .value_name("NETWORK_RANGE") + .help("Network range to scan") + ) + .arg( + Arg::new("file").short('f').long("file") + .value_name("FILE_PATH") + .conflicts_with("network") + .help("Read IPv4 addresses from a file") + ) + .arg( + Arg::new("timeout").short('t').long("timeout") + .value_name("TIMEOUT_DURATION") + .help("ARP response timeout") + ) + .arg( + Arg::new("source_ip").short('S').long("source-ip") + .value_name("SOURCE_IPV4") + .help("Source IPv4 address for requests") + ) + .arg( + Arg::new("destination_mac").short('M').long("dest-mac") + .value_name("DESTINATION_MAC") + .help("Destination MAC address for requests") + ) + .arg( + Arg::new("source_mac").long("source-mac") + .value_name("SOURCE_MAC") + .help("Source MAC address for requests") + ) + .arg( + Arg::new("numeric").long("numeric") + .action(ArgAction::SetTrue) + .help("Numeric mode, no hostname resolution") + ) + .arg( + Arg::new("vlan").short('Q').long("vlan") + .value_name("VLAN_ID") + .help("Send using 802.1Q with VLAN ID") + ) + .arg( + Arg::new("retry_count").short('r').long("retry") + .value_name("RETRY_COUNT") + .help("Host retry attempt count") + ) + .arg( + Arg::new("random").short('R').long("random") + .action(ArgAction::SetTrue) + .help("Randomize the target list") + ) + .arg( + Arg::new("interval").short('I').long("interval") + .value_name("INTERVAL_DURATION") + .help("Milliseconds between ARP requests") + ) + .arg( + Arg::new("bandwidth").short('B').long("bandwidth") + .value_name("BITS") + .conflicts_with("interval") + .help("Limit scan bandwidth (bits/second)") + ) + .arg( + Arg::new("oui-file").long("oui-file") + .value_name("FILE_PATH") + .help("Path to custom IEEE OUI CSV file") + ) + .arg( + Arg::new("list").short('l').long("list") + .action(ArgAction::SetTrue) + .exclusive(true) + .help("List network interfaces") + ) + .arg( + Arg::new("output").short('o').long("output") + .value_name("FORMAT") + .help("Define output format") + ) + .arg( + Arg::new("hw_type").long("hw-type") + .value_name("HW_TYPE") + .help("Custom ARP hardware field") + ) + .arg( + Arg::new("hw_addr").long("hw-addr") + .value_name("ADDRESS_LEN") + .help("Custom ARP hardware address length") + ) + .arg( + Arg::new("proto_type").long("proto-type") + .value_name("PROTO_TYPE") + .help("Custom ARP proto type") + ) + .arg( + Arg::new("proto_addr").long("proto-addr") + .value_name("ADDRESS_LEN") + .help("Custom ARP proto address length") + ) + .arg( + Arg::new("arp_operation").long("arp-op") + .value_name("OPERATION_ID") + .help("Custom ARP operation ID") + ) + .arg( + Arg::new("packet_help").long("packet-help") + .action(ArgAction::SetTrue) + .exclusive(true) + .help("Print details about an ARP packet") + ) + .after_help(EXAMPLES_HELP) +} + +pub enum OutputFormat { + Plain, + Json, + Yaml, + Csv +} + +pub enum ProfileType { + Default, + Fast, + Stealth, + Chaos +} + +pub enum ScanTiming { + Interval(u64), + Bandwidth(u64) +} + +pub struct ScanOptions { + pub profile: ProfileType, + pub interface_name: Option, + pub network_range: Option>, + pub timeout_ms: u64, + pub resolve_hostname: bool, + pub source_ipv4: Option, + pub source_mac: Option, + pub destination_mac: Option, + pub vlan_id: Option, + pub retry_count: usize, + pub scan_timing: ScanTiming, + pub randomize_targets: bool, + pub output: OutputFormat, + pub oui_file: String, + pub hw_type: Option, + pub hw_addr: Option, + pub proto_type: Option, + pub proto_addr: Option, + pub arp_operation: Option, + pub packet_help: bool, +} + +impl ScanOptions { + + fn list_required_networks(file_value: Option<&String>, network_value: Option<&String>) -> Result>, String> { + + let network_options = (file_value, network_value); + match network_options { + (Some(file_path), None) => { + + let path = Path::new(file_path); + fs::read_to_string(path).map(|content| { + Some(content.lines().map(|line| line.to_string()).collect()) + }).map_err(|err| { + format!("Could not open file {} - {}", file_path, err) + }) + + }, + (None, Some(raw_ranges)) => { + Ok(Some(raw_ranges.split(',').map(|line| line.to_string()).collect())) + }, + _ => Ok(None) + } + } + + /** + * Computes the whole network range requested by the user through CLI + * arguments or files. This method will fail of a failure has been detected + * (either on the IO level or the network syntax parsing) + */ + fn compute_networks(file_value: Option<&String>, network_value: Option<&String>) -> Result>, String> { + + let required_networks: Option> = ScanOptions::list_required_networks(file_value, network_value)?; + if required_networks.is_none() { + return Ok(None); + } + + let mut networks: Vec = vec![]; + for network_text in required_networks.unwrap() { + + match IpNetwork::from_str(&network_text) { + Ok(parsed_network) => { + networks.push(parsed_network); + Ok(()) + }, + Err(err) => { + Err(format!("Expected valid IPv4 network range ({})", err)) + } + }?; + } + Ok(Some(networks)) + } + + /** + * Computes scan timing constraints, as requested by the user through CLI + * arguments. The scan timing constraints will be either expressed in bandwidth + * (bits per second) or interval between ARP requests (in milliseconds). + */ + fn compute_scan_timing(matches: &ArgMatches, profile: &ProfileType) -> ScanTiming { + + match (matches.get_one::("bandwidth"), matches.get_one::("interval")) { + (Some(bandwidth_text), None) => { + let bits_second: u64 = bandwidth_text.parse().unwrap_or_else(|err| { + eprintln!("Expected positive number, {}", err); + process::exit(1); + }); + ScanTiming::Bandwidth(bits_second) + }, + (None, Some(interval_text)) => parse_to_milliseconds(interval_text).map(ScanTiming::Interval).unwrap_or_else(|err| { + eprintln!("Expected correct interval, {}", err); + process::exit(1); + }), + _ => match profile { + ProfileType::Stealth => ScanTiming::Interval(REQUEST_MS_INTERVAL * 2), + ProfileType::Fast => ScanTiming::Interval(0), + _ => ScanTiming::Interval(REQUEST_MS_INTERVAL) + } + } + } + + /** + * Build a new 'ScanOptions' struct that will be used in the whole CLI such + * as the network level, the display details and more. The scan options reflect + * user requests for the CLI and should not be mutated. + */ + pub fn new(matches: &ArgMatches) -> Arc { + + let profile = match matches.get_one::("profile") { + Some(output_request) => { + + match output_request.as_ref() { + "default" | "d" => ProfileType::Default, + "fast" | "f" => ProfileType::Fast, + "stealth" | "s" => ProfileType::Stealth, + "chaos" | "c" => ProfileType::Chaos, + _ => { + eprintln!("Expected correct profile name (default/fast/stealth/chaos)"); + process::exit(1); + } + } + }, + None => ProfileType::Default + }; + + let interface_name = matches.get_one::("interface").cloned(); + + let file_option = matches.get_one::("file"); + let network_option = matches.get_one::("network"); + + let network_range = ScanOptions::compute_networks(file_option, network_option).unwrap_or_else(|err| { + eprintln!("Could not compute requested network range to scan"); + eprintln!("{}", err); + process::exit(1); + }); + + let timeout_ms: u64 = match matches.get_one::("timeout") { + Some(timeout_text) => parse_to_milliseconds(timeout_text).unwrap_or_else(|err| { + eprintln!("Expected correct timeout, {}", err); + process::exit(1); + }), + None => match profile { + ProfileType::Fast => TIMEOUT_MS_FAST, + _ => TIMEOUT_MS_DEFAULT + } + }; + + // Hostnames will not be resolved in numeric mode or stealth profile + let resolve_hostname = !matches.get_flag("numeric") && !matches!(profile, ProfileType::Stealth); + + let source_ipv4: Option = match matches.get_one::("source_ip") { + Some(source_ip) => { + + match source_ip.parse::() { + Ok(parsed_ipv4) => Some(parsed_ipv4), + Err(_) => { + eprintln!("Expected valid IPv4 as source IP"); + process::exit(1); + } + } + }, + None => None + }; + + let destination_mac: Option = match matches.get_one::("destination_mac") { + Some(mac_address) => { + + match mac_address.parse::() { + Ok(parsed_mac) => Some(parsed_mac), + Err(_) => { + eprintln!("Expected valid MAC address as destination"); + process::exit(1); + } + } + }, + None => None + }; + + let source_mac: Option = match matches.get_one::("source_mac") { + Some(mac_address) => { + + match mac_address.parse::() { + Ok(parsed_mac) => Some(parsed_mac), + Err(_) => { + eprintln!("Expected valid MAC address as source"); + process::exit(1); + } + } + }, + None => None + }; + + let vlan_id: Option = match matches.get_one::("vlan") { + Some(vlan) => { + + match vlan.parse::() { + Ok(vlan_number) => Some(vlan_number), + Err(_) => { + eprintln!("Expected valid VLAN identifier"); + process::exit(1); + } + } + }, + None => None + }; + + let retry_count = match matches.get_one::("retry_count") { + Some(retry_count) => { + + match retry_count.parse::() { + Ok(retry_number) => retry_number, + Err(_) => { + eprintln!("Expected positive number for host retry count"); + process::exit(1); + } + } + }, + None => match profile { + ProfileType::Chaos => HOST_RETRY_DEFAULT * 2, + _ => HOST_RETRY_DEFAULT + } + }; + + let scan_timing: ScanTiming = ScanOptions::compute_scan_timing(matches, &profile); + + let output = match matches.get_one::("output") { + Some(output_request) => { + + match output_request.as_ref() { + "json" => OutputFormat::Json, + "yaml" => OutputFormat::Yaml, + "plain" | "text" => OutputFormat::Plain, + "csv" => OutputFormat::Csv, + _ => { + eprintln!("Expected correct output format (json/yaml/plain)"); + process::exit(1); + } + } + }, + None => OutputFormat::Plain + }; + + let randomize_targets = matches.get_flag("random") || matches!(profile, ProfileType::Stealth | ProfileType::Chaos); + + let oui_file: String = match matches.get_one::("oui-file") { + Some(file) => file.to_string(), + None => "/usr/share/arp-scan/ieee-oui.csv".to_string() + }; + + let hw_type = match matches.get_one::("hw_type") { + Some(hw_type_text) => { + + match hw_type_text.parse::() { + Ok(type_number) => Some(ArpHardwareType::new(type_number)), + Err(_) => { + eprintln!("Expected valid ARP hardware type number"); + process::exit(1); + } + } + }, + None => None + }; + + let hw_addr = match matches.get_one::("hw_addr") { + Some(hw_addr_text) => { + + match hw_addr_text.parse::() { + Ok(addr_length) => Some(addr_length), + Err(_) => { + eprintln!("Expected valid ARP hardware address length"); + process::exit(1); + } + } + }, + None => None + }; + + let proto_type = match matches.get_one::("proto_type") { + Some(proto_type_text) => { + + match proto_type_text.parse::() { + Ok(type_number) => Some(EtherType::new(type_number)), + Err(_) => { + eprintln!("Expected valid ARP proto type number"); + process::exit(1); + } + } + }, + None => None + }; + + let proto_addr = match matches.get_one::("proto_addr") { + Some(proto_addr_text) => { + + match proto_addr_text.parse::() { + Ok(addr_length) => Some(addr_length), + Err(_) => { + eprintln!("Expected valid ARP hardware address length"); + process::exit(1); + } + } + }, + None => None + }; + + let arp_operation = match matches.get_one::("arp_operation") { + Some(arp_op_text) => { + + match arp_op_text.parse::() { + Ok(op_number) => Some(ArpOperation::new(op_number)), + Err(_) => { + eprintln!("Expected valid ARP operation number"); + process::exit(1); + } + } + }, + None => None + }; + + let packet_help = matches.get_flag("packet_help"); + + Arc::new(ScanOptions { + profile, + interface_name, + network_range, + timeout_ms, + resolve_hostname, + source_ipv4, + destination_mac, + source_mac, + vlan_id, + retry_count, + scan_timing, + randomize_targets, + output, + oui_file, + hw_type, + hw_addr, + proto_type, + proto_addr, + arp_operation, + packet_help, + }) + } + + pub fn is_plain_output(&self) -> bool { + + matches!(&self.output, OutputFormat::Plain) + } + + pub fn has_vlan(&self) -> bool { + + matches!(&self.vlan_id, Some(_)) + } + + pub fn request_protocol_print(&self) -> bool { + self.packet_help + } + +} + + +#[cfg(test)] +mod tests { + + use super::*; + use ipnetwork::Ipv4Network; + + #[test] + fn should_have_no_network_default() { + + let networks = ScanOptions::compute_networks(None, None); + assert_eq!(networks, Ok(None)); + } + + #[test] + fn should_handle_single_ipv4_arg() { + + let networks = ScanOptions::compute_networks(None, Some(&"192.168.1.20".to_string())); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 20), 32).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_handle_multiple_ipv4_arg() { + + let networks = ScanOptions::compute_networks(None, Some(&"192.168.1.20,192.168.1.50".to_string())); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 20), 32).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 50), 32).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_handle_single_network_arg() { + + let networks = ScanOptions::compute_networks(None, Some(&"192.168.1.0/24".to_string())); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 0), 24).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_handle_network_mix_arg() { + + let networks = ScanOptions::compute_networks(None, Some(&"192.168.20.1,192.168.1.0/24,192.168.5.4/28".to_string())); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 20, 1), 32).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 0), 24).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 5, 4), 28).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_handle_file_input() { + + let networks = ScanOptions::compute_networks(Some(&"./data/ip-list.txt".to_string()), None); + + let target_network: Vec = vec![ + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 32).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 2), 32).unwrap() + ), + IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 2, 0), 29).unwrap() + ) + ]; + + assert_eq!(networks, Ok(Some(target_network))); + } + + #[test] + fn should_fail_incorrect_network() { + + let networks = ScanOptions::compute_networks(None, Some(&"500.10.10.10/24".to_string())); + + assert_eq!(networks, Err("Expected valid IPv4 network range (invalid address: 500.10.10.10/24)".to_string())); + } + + #[test] + fn should_fail_unreadable_network() { + + let networks = ScanOptions::compute_networks(None, Some(&"no-network".to_string())); + + assert_eq!(networks, Err("Expected valid IPv4 network range (invalid address: no-network)".to_string())); + } + +} diff --git a/src/main.rs b/src/main.rs index 5d4f460..d85cc26 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,176 +1,166 @@ -use pnet::datalink; -use pnet::packet::arp::{MutableArpPacket, ArpPacket, ArpOperations, ArpHardwareTypes}; -use pnet::packet::ethernet::{MutableEthernetPacket, EtherTypes}; -use pnet::packet::MutablePacket; -use pnet::packet::ethernet::EthernetPacket; -use pnet::packet::Packet; +mod args; +mod network; +mod time; +mod utils; +mod vendor; -use clap::{Arg, App}; - -use std::net::{IpAddr, Ipv4Addr}; +use std::net::IpAddr; use std::process; use std::thread; -use std::time::Instant; +use std::sync::Arc; +use std::time::Duration; +use std::sync::atomic::{AtomicBool, Ordering}; -fn is_root_user() -> bool { - std::env::var("USER").unwrap_or(String::from("")) == String::from("root") -} +use crate::args::{ScanOptions, OutputFormat}; +use crate::network::NetworkIterator; +use crate::vendor::Vendor; fn main() { + let matches = args::build_args().get_matches(); - if !is_root_user() { - eprintln!("Should run this binary as root"); - process::exit(1); - } - - let matches = App::new("arp-scan") - .version("0.1") - .about("A minimalistic ARP scan tool written in Rust") - .arg(Arg::with_name("interface").short("i").long("interface").takes_value(true).value_name("INTERFACE_NAME").help("Network interface")) - .arg(Arg::with_name("timeout").short("t").long("timeout").takes_value(true).value_name("TIMEOUT_SECONDS").help("ARP response timeout")) - .get_matches(); - - let interface_name = match matches.value_of("interface") { - Some(name) => name, - None => { - eprintln!("Interface name required"); - process::exit(1); - } - }; - - let timeout_seconds: u64 = match matches.value_of("timeout") { - Some(seconds) => seconds.parse().unwrap_or(10), - None => 10 - }; + // Find interfaces & list them if requested + // ---------------------------------------- + // All network interfaces are retrieved and will be listed if the '--list' + // flag has been given in the request. Note that this can be done without + // using a root account (this will be verified later). - // ---------------------- + let interfaces = pnet_datalink::interfaces(); - let interfaces = datalink::interfaces(); + if matches.get_flag("list") { + utils::show_interfaces(&interfaces); + process::exit(0); + } - let selected_interface: &datalink::NetworkInterface = interfaces.iter() - .find(|interface| { interface.name == interface_name && interface.is_up() && !interface.is_loopback() }) - .unwrap_or_else(|| { - eprintln!("Could not find interface with name {}", interface_name); - process::exit(1); - }); + // Assert requirements for a local network scan + // -------------------------------------------- + // Ensure all requirements are met to perform an ARP scan on the local + // network for the given interface. ARP scans require an active interface + // with an IPv4 address and root permissions (for crafting ARP packets). - let ip_network = match selected_interface.ips.first() { - Some(ip_network) => ip_network, - None => { - eprintln!("Expects a valid IP on the interface"); - process::exit(1); - } - }; + let scan_options = ScanOptions::new(&matches); - if !ip_network.is_ipv4() { - eprintln!("Only IPv4 supported"); - process::exit(1); + if scan_options.request_protocol_print() { + utils::print_ascii_packet(); + process::exit(0); } - println!("Selected interface {} with IP {}", selected_interface.name, ip_network); - - // ----------------------- + // Upgrade user privileges when needed + // ---------------------------------------- + // Providing a prompt for the user when + // the app is run and user is not root + sudo::escalate_if_needed().expect("You need root permissions to run this app. Unable to escalate to sudo"); + + // Get network configuration + // ------------------------- + // See args.rs and in particular the struct ScanOptions + // for a full list of options + let (selected_interface, ip_networks) = network::compute_network_configuration(&interfaces, &scan_options); + + if scan_options.is_plain_output() { + utils::display_prescan_details(&ip_networks, selected_interface, scan_options.clone()); + } - let (mut tx, mut rx) = match datalink::channel(selected_interface, Default::default()) { - Ok(datalink::Channel::Ethernet(tx, rx)) => (tx, rx), - Ok(_) => panic!("unknown type"), - Err(error) => panic!(error) + // Start ARP scan operation + // ------------------------ + // ARP responses on the interface will be collected in a separate thread, + // while the main thread sends a batch of ARP requests for each IP in the + // local network. + let channel_config = pnet_datalink::Config { + read_timeout: Some(Duration::from_millis(network::DATALINK_RCV_TIMEOUT)), + ..pnet_datalink::Config::default() }; - let responses = thread::spawn(move || { - - let start_recording = Instant::now(); + let (mut tx, mut rx) = match pnet_datalink::channel(selected_interface, channel_config) { + Ok(pnet_datalink::Channel::Ethernet(tx, rx)) => (tx, rx), + Ok(_) => { + eprintln!("Expected an Ethernet datalink channel"); + process::exit(1); + }, + Err(error) => { + eprintln!("Datalink channel creation failed ({})", error); + process::exit(1); + } + }; - loop { + // The 'timed_out' mutex is shared accross the main thread (which performs + // ARP packet sending) and the response thread (which receives and stores + // all ARP responses). + let timed_out = Arc::new(AtomicBool::new(false)); + let cloned_timed_out = Arc::clone(&timed_out); - if start_recording.elapsed().as_secs() > timeout_seconds { - break; - } - - let arp_buffer = rx.next().unwrap_or_else(|error| { - eprintln!("Failed to receive ARP requests ({})", error); - process::exit(1); - }); - - let ethernet_packet = match EthernetPacket::new(&arp_buffer[..]) { - Some(packet) => packet, - None => continue - }; - - let is_arp = match ethernet_packet.get_ethertype() { - EtherTypes::Arp => true, - _ => false - }; - - if !is_arp { - continue; - } + let mut vendor_list = Vendor::new(&scan_options.oui_file); - let arp_packet = ArpPacket::new(&arp_buffer[MutableEthernetPacket::minimum_packet_size()..]); + let cloned_options = Arc::clone(&scan_options); + let arp_responses = thread::spawn(move || network::receive_arp_responses(&mut rx, cloned_options, cloned_timed_out, &mut vendor_list)); - match arp_packet { - Some(arp) => println!("{} - {}", arp.get_sender_proto_addr(), arp.get_sender_hw_addr()), - _ => () - } - } + let network_size = utils::compute_network_size(&ip_networks); - }); + let estimations = network::compute_scan_estimation(network_size, &scan_options); + let interval_ms = estimations.interval_ms; - println!("Sending {:?} ARP requests to network", ip_network.size()); - for ip_address in ip_network.iter() { + if scan_options.is_plain_output() { - if let IpAddr::V4(ipv4_address) = ip_address { - send_arp_request(&mut tx, selected_interface, ipv4_address); - } + let formatted_ms = time::format_milliseconds(estimations.duration_ms); + println!("Estimated scan time {} ({} bytes, {} bytes/s)", formatted_ms, estimations.request_size, estimations.bandwidth); + println!("Sending {} ARP requests (waiting at least {}ms, {}ms request interval)", network_size, scan_options.timeout_ms, interval_ms); } - // ------------------ + let has_reached_timeout = Arc::new(AtomicBool::new(false)); + let cloned_reached_timeout = Arc::clone(&has_reached_timeout); - responses.join().unwrap_or_else(|error| { - eprintln!("Failed to close receive thread ({:?})", error); + ctrlc::set_handler(move || { + eprintln!("[warn] Receiving halt signal, ending scan with partial results"); + cloned_reached_timeout.store(true, Ordering::Relaxed); + }).unwrap_or_else(|err| { + eprintln!("Could not set CTRL+C handler ({})", err); process::exit(1); }); -} -fn send_arp_request(tx: &mut Box, interface: &datalink::NetworkInterface, target_ip: Ipv4Addr) { + let source_ip = network::find_source_ip(selected_interface, scan_options.source_ipv4); - let mut ethernet_buffer = [0u8; 42]; - let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap(); + // The retry count does right now use a 'brute-force' strategy without + // synchronization process with the already known hosts. + for _ in 0..scan_options.retry_count { - let target_mac = datalink::MacAddr::broadcast(); - let source_mac = interface.mac.unwrap_or_else(|| { - eprintln!("Interface should have a MAC address"); - process::exit(1); - }); + if has_reached_timeout.load(Ordering::Relaxed) { + break; + } - ethernet_packet.set_destination(target_mac); - ethernet_packet.set_source(source_mac); - ethernet_packet.set_ethertype(EtherTypes::Arp); + let ip_addresses = NetworkIterator::new(&ip_networks, scan_options.randomize_targets); - let mut arp_buffer = [0u8; 28]; - let mut arp_packet = MutableArpPacket::new(&mut arp_buffer).unwrap(); + for ip_address in ip_addresses { - let source_ip = interface.ips.first().unwrap_or_else(|| { - eprintln!("Interface should have an IP address"); - process::exit(1); - }).ip(); + if has_reached_timeout.load(Ordering::Relaxed) { + break; + } - let source_ipv4 = match source_ip { - IpAddr::V4(ipv4_addr) => Some(ipv4_addr), - IpAddr::V6(_ipv6_addr) => None - }; + if let IpAddr::V4(ipv4_address) = ip_address { + network::send_arp_request(&mut tx, selected_interface, source_ip, ipv4_address, Arc::clone(&scan_options)); + thread::sleep(Duration::from_millis(interval_ms)); + } + } + } - arp_packet.set_hardware_type(ArpHardwareTypes::Ethernet); - arp_packet.set_protocol_type(EtherTypes::Ipv4); - arp_packet.set_hw_addr_len(6); - arp_packet.set_proto_addr_len(4); - arp_packet.set_operation(ArpOperations::Request); - arp_packet.set_sender_hw_addr(source_mac); - arp_packet.set_sender_proto_addr(source_ipv4.unwrap()); - arp_packet.set_target_hw_addr(target_mac); - arp_packet.set_target_proto_addr(target_ip); + // Once the ARP packets are sent, the main thread will sleep for T seconds + // (where T is the timeout option). After the sleep phase, the response + // thread will receive a stop request through the 'timed_out' mutex. + let mut sleep_ms_mount: u64 = 0; + while !has_reached_timeout.load(Ordering::Relaxed) && sleep_ms_mount < scan_options.timeout_ms { + + thread::sleep(Duration::from_millis(100)); + sleep_ms_mount += 100; + } + timed_out.store(true, Ordering::Relaxed); - ethernet_packet.set_payload(arp_packet.packet_mut()); + let (response_summary, target_details) = arp_responses.join().unwrap_or_else(|error| { + eprintln!("Failed to close receive thread ({:?})", error); + process::exit(1); + }); - tx.send_to(ðernet_packet.to_immutable().packet(), Some(interface.clone())); + match &scan_options.output { + OutputFormat::Plain => utils::display_scan_results(response_summary, target_details, &scan_options), + OutputFormat::Json => println!("{}", utils::export_to_json(response_summary, target_details)), + OutputFormat::Yaml => println!("{}", utils::export_to_yaml(response_summary, target_details)), + OutputFormat::Csv => print!("{}", utils::export_to_csv(response_summary, target_details)) + } } diff --git a/src/network.rs b/src/network.rs new file mode 100644 index 0000000..0b2edda --- /dev/null +++ b/src/network.rs @@ -0,0 +1,602 @@ +use std::process; +use std::net::{IpAddr, Ipv4Addr}; +use std::time::Instant; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::io::ErrorKind::TimedOut; +use std::convert::TryInto; + +use dns_lookup::lookup_addr; +use ipnetwork::IpNetwork; +use pnet_datalink::{MacAddr, NetworkInterface, DataLinkSender, DataLinkReceiver}; +use pnet::packet::{MutablePacket, Packet}; +use pnet::packet::ethernet::{EthernetPacket, MutableEthernetPacket, EtherTypes}; +use pnet::packet::arp::{MutableArpPacket, ArpOperations, ArpHardwareTypes, ArpPacket}; +use pnet::packet::vlan::{ClassOfService, MutableVlanPacket}; +use rand::prelude::*; + +use crate::args::ScanOptions; +use crate::vendor::Vendor; +use crate::utils; +use crate::args::ScanTiming; + +pub const DATALINK_RCV_TIMEOUT: u64 = 500; + +const VLAN_QOS_DEFAULT: u8 = 1; +const ARP_PACKET_SIZE: usize = 28; +const VLAN_PACKET_SIZE: usize = 32; + +const ETHERNET_STD_PACKET_SIZE: usize = 42; +const ETHERNET_VLAN_PACKET_SIZE: usize = 46; + +/** + * Contains scan estimation records. This will be computed before the scan + * starts and should give insights about the scan. + */ +pub struct ScanEstimation { + pub interval_ms: u64, + pub duration_ms: u128, + pub request_size: u128, + pub bandwidth: u128 +} + +/** + * Gives high-level details about the scan response. This may include Ethernet + * details (packet count, size, ...) and other technical network aspects. + */ +pub struct ResponseSummary { + pub packet_count: usize, + pub arp_count: usize, + pub duration_ms: u128 +} + +/** + * A target detail represents a single host on the local network with an IPv4 + * address and a linked MAC address. Hostnames are optional since some hosts + * does not respond to the resolve call (or the numeric mode may be enabled). + */ +pub struct TargetDetails { + pub ipv4: Ipv4Addr, + pub mac: MacAddr, + pub hostname: Option, + pub vendor: Option +} + +/** + * Compute a network configuration based on the scan options and available + * interfaces. This configuration will be used in the scan process to target a + * specific network on a network interfaces. + */ +pub fn compute_network_configuration<'a>(interfaces: &'a [NetworkInterface], scan_options: &'a Arc) -> (&'a NetworkInterface, Vec<&'a IpNetwork>) { + + let interface_name = match &scan_options.interface_name { + Some(name) => String::from(name), + None => { + + let name = utils::select_default_interface(interfaces).map(|interface| interface.name); + + match name { + Some(name) => name, + None => { + eprintln!("Could not find a default network interface"); + eprintln!("Use 'arp scan -l' to list available interfaces"); + process::exit(1); + } + } + } + }; + + let selected_interface: &NetworkInterface = interfaces.iter() + .find(|interface| { interface.name == interface_name && interface.is_up() && !interface.is_loopback() }) + .unwrap_or_else(|| { + eprintln!("Could not find interface with name {}", interface_name); + eprintln!("Make sure the interface is up, not loopback and has a valid IPv4"); + process::exit(1); + }); + + let ip_networks: Vec<&ipnetwork::IpNetwork> = match &scan_options.network_range { + Some(network_range) => network_range.iter().collect(), + None => selected_interface.ips.iter().filter(|ip_network| ip_network.is_ipv4()).collect() + }; + + (selected_interface, ip_networks) +} + +/** + * Based on the network size and given scan options, this function performs an + * estimation of the scan impact (timing, bandwidth, ...). Keep in mind that + * this is only an estimation, real results may vary based on the network. + */ +pub fn compute_scan_estimation(host_count: u128, options: &Arc) -> ScanEstimation { + + let timeout: u128 = options.timeout_ms.into(); + let packet_size: u128 = match options.has_vlan() { + true => ETHERNET_VLAN_PACKET_SIZE.try_into().expect("Internal number conversion failed for VLAN packet size"), + false => ETHERNET_STD_PACKET_SIZE.try_into().expect("Internal number conversion failed for Ethernet packet size") + }; + let retry_count: u128 = options.retry_count.try_into().unwrap_or_else(|err| { + eprintln!("[warn] Could not cast retry count, defaults to 1 - {}", err); + 1 + }); + + // The values below are averages based on an amount of performed network + // scans. This may of course vary based on network configurations. + let avg_arp_request_ms: u128 = 3; + let avg_resolve_ms = 500; + + let request_size: u128 = host_count * packet_size; + + // Either the user provides an interval (expressed in milliseconds), either + // he provides a bandwidth (in bits per second) or either we are using the + // default interval. The goal of the code below is to compute the interval + // & bandwidth, based on the given inputs. Note that the computations in + // each match arm are therefore linked (but rewritten, based on the inputs). + let (interval_ms, bandwidth, request_phase_ms): (u64, u128, u128) = match options.scan_timing { + ScanTiming::Bandwidth(bandwidth) => { + + let bandwidth_lg: u128 = bandwidth.into(); + let request_phase_ms: u128 = (request_size * 1000) / bandwidth_lg; + let interval_ms: u128 = (request_phase_ms/retry_count/host_count) - avg_arp_request_ms; + + (interval_ms.try_into().unwrap(), bandwidth_lg, request_phase_ms) + + }, + ScanTiming::Interval(interval) => { + + let interval_ms_lg: u128 = interval.into(); + let request_phase_ms: u128 = (host_count * (avg_arp_request_ms + interval_ms_lg)) * retry_count; + let bandwidth = (request_size * 1000) / request_phase_ms; + + (interval, bandwidth, request_phase_ms) + } + }; + + let duration_ms = request_phase_ms + timeout + avg_resolve_ms; + + ScanEstimation { + interval_ms, + duration_ms, + request_size, + bandwidth + } +} + +/** + * Send a single ARP request - using a datalink-layer sender, a given network + * interface and a target IPv4 address. The ARP request will be broadcasted to + * the whole local network with the first valid IPv4 address on the interface. + */ +pub fn send_arp_request(tx: &mut Box, interface: &NetworkInterface, source_ip: Ipv4Addr, target_ip: Ipv4Addr, options: Arc) { + + let mut ethernet_buffer = match options.has_vlan() { + true => vec![0u8; ETHERNET_VLAN_PACKET_SIZE], + false => vec![0u8; ETHERNET_STD_PACKET_SIZE] + }; + let mut ethernet_packet = MutableEthernetPacket::new(&mut ethernet_buffer).unwrap_or_else(|| { + eprintln!("Could not build Ethernet packet"); + process::exit(1); + }); + + let target_mac = match options.destination_mac { + Some(forced_mac) => forced_mac, + None => MacAddr::broadcast() + }; + let source_mac = match options.source_mac { + Some(forced_source_mac) => forced_source_mac, + None => interface.mac.unwrap_or_else(|| { + eprintln!("Interface should have a MAC address"); + process::exit(1); + }) + }; + + ethernet_packet.set_destination(target_mac); + ethernet_packet.set_source(source_mac); + + let selected_ethertype = match options.vlan_id { + Some(_) => EtherTypes::Vlan, + None => EtherTypes::Arp + }; + ethernet_packet.set_ethertype(selected_ethertype); + + let mut arp_buffer = [0u8; ARP_PACKET_SIZE]; + let mut arp_packet = MutableArpPacket::new(&mut arp_buffer).unwrap_or_else(|| { + eprintln!("Could not build ARP packet"); + process::exit(1); + }); + + arp_packet.set_hardware_type(options.hw_type.unwrap_or(ArpHardwareTypes::Ethernet)); + arp_packet.set_protocol_type(options.proto_type.unwrap_or(EtherTypes::Ipv4)); + arp_packet.set_hw_addr_len(options.hw_addr.unwrap_or(6)); + arp_packet.set_proto_addr_len(options.proto_addr.unwrap_or(4)); + arp_packet.set_operation(options.arp_operation.unwrap_or(ArpOperations::Request)); + arp_packet.set_sender_hw_addr(source_mac); + arp_packet.set_sender_proto_addr(source_ip); + arp_packet.set_target_hw_addr(target_mac); + arp_packet.set_target_proto_addr(target_ip); + + if let Some(vlan_id) = options.vlan_id { + + let mut vlan_buffer = [0u8; VLAN_PACKET_SIZE]; + let mut vlan_packet = MutableVlanPacket::new(&mut vlan_buffer).unwrap_or_else(|| { + eprintln!("Could not build VLAN packet"); + process::exit(1); + }); + vlan_packet.set_vlan_identifier(vlan_id); + vlan_packet.set_priority_code_point(ClassOfService::new(VLAN_QOS_DEFAULT)); + vlan_packet.set_drop_eligible_indicator(0); + vlan_packet.set_ethertype(EtherTypes::Arp); + + vlan_packet.set_payload(arp_packet.packet_mut()); + + ethernet_packet.set_payload(vlan_packet.packet_mut()); + } + else { + ethernet_packet.set_payload(arp_packet.packet_mut()); + } + + tx.send_to(ethernet_packet.to_immutable().packet(), Some(interface.clone())); +} + +/** + * A network iterator for iterating over multiple network ranges in with a + * low-memory approach. This iterator was crafted to allow iteration over huge + * network ranges (192.168.0.0/16) without consuming excessive memory. + */ +pub struct NetworkIterator { + current_iterator: Option, + networks: Vec, + is_random: bool, + random_pool: Vec +} + +impl NetworkIterator { + + pub fn new(networks_ref: &[&IpNetwork], is_random: bool) -> NetworkIterator { + + // The IpNetwork struct implements the Clone trait, which means that a simple + // dereference will clone the struct in the new vector + let mut networks: Vec = networks_ref.iter().map(|network| *(*network)).collect(); + + if is_random { + let mut rng = rand::thread_rng(); + networks.shuffle(&mut rng); + } + + NetworkIterator { + current_iterator: None, + networks, + is_random, + random_pool: vec![] + } + } + + /** + * The functions below are not public and only used by the Iterator trait + * to help keep the next() code clean. + */ + + fn has_no_items_left(&self) -> bool { + self.current_iterator.is_none() && self.networks.is_empty() && self.random_pool.is_empty() + } + + fn fill_random_pool(&mut self) { + + for _ in 0..1000 { + + let next_ip = self.current_iterator.as_mut().unwrap().next(); + if next_ip.is_none() { + break; + } + + self.random_pool.push(next_ip.unwrap()); + } + + let mut rng = rand::thread_rng(); + self.random_pool.shuffle(&mut rng); + } + + fn select_new_iterator(&mut self) { + + self.current_iterator = Some(self.networks.remove(0).iter()); + } + + fn pop_next_iterator_address(&mut self) -> Option { + + self.current_iterator.as_mut().map(|iterator| iterator.next()).unwrap_or(None) + } + +} + +impl Iterator for NetworkIterator { + + type Item = IpAddr; + + fn next(&mut self) -> Option { + + if self.has_no_items_left() { + return None; + } + + if self.current_iterator.is_none() && !self.networks.is_empty() { + self.select_new_iterator(); + } + + if self.is_random && self.random_pool.is_empty() { + self.fill_random_pool(); + } + + let next_ip = match self.is_random { + true => self.random_pool.pop(), + false => self.pop_next_iterator_address() + }; + + if next_ip.is_none() && !self.networks.is_empty() { + self.select_new_iterator(); + return self.pop_next_iterator_address(); + } + + next_ip + } +} + +/** + * Find the most adequate IPv4 address on a given network interface for sending + * ARP requests. If the 'forced_source_ipv4' parameter is set, it will take + * the priority over the network interface address. + */ +pub fn find_source_ip(network_interface: &NetworkInterface, forced_source_ipv4: Option) -> Ipv4Addr { + + if let Some(forced_ipv4) = forced_source_ipv4 { + return forced_ipv4; + } + + let potential_network = network_interface.ips.iter().find(|network| network.is_ipv4()); + match potential_network.map(|network| network.ip()) { + Some(IpAddr::V4(ipv4_addr)) => ipv4_addr, + _ => { + eprintln!("Expected IPv4 address on network interface"); + process::exit(1); + } + } +} + +/** + * Wait at least N seconds and receive ARP network responses. The main + * downside of this function is the blocking nature of the datalink receiver: + * when the N seconds are elapsed, the receiver loop will therefore only stop + * on the next received frame. Therefore, the receiver should have been + * configured to stop at certain intervals (500ms for example). + */ +pub fn receive_arp_responses(rx: &mut Box, options: Arc, timed_out: Arc, vendor_list: &mut Vendor) -> (ResponseSummary, Vec) { + + let mut discover_map: HashMap = HashMap::new(); + let start_recording = Instant::now(); + + let mut packet_count = 0; + let mut arp_count = 0; + + loop { + + if timed_out.load(Ordering::Relaxed) { + break; + } + + let arp_buffer = match rx.next() { + Ok(buffer) => buffer, + Err(error) => { + match error.kind() { + // The 'next' call will only block the thread for a given + // amount of microseconds. The goal is to avoid long blocks + // due to the lack of packets received. + TimedOut => continue, + _ => { + eprintln!("Failed to receive ARP requests ({})", error); + process::exit(1); + } + }; + } + }; + packet_count += 1; + + let ethernet_packet = match EthernetPacket::new(arp_buffer) { + Some(packet) => packet, + None => continue + }; + + let is_arp_type = matches!(ethernet_packet.get_ethertype(), EtherTypes::Arp); + if !is_arp_type { + continue; + } + + let arp_packet = ArpPacket::new(&arp_buffer[MutableEthernetPacket::minimum_packet_size()..]); + arp_count += 1; + + // If we found an ARP packet, extract the details and add the essential + // fields in the discover map. Please note that results are grouped by + // IPv4 address - which means that a MAC change will appear as two + // separete records in the result table. + if let Some(arp) = arp_packet { + + let sender_ipv4 = arp.get_sender_proto_addr(); + let sender_mac = arp.get_sender_hw_addr(); + + discover_map.insert(sender_ipv4, TargetDetails { + ipv4: sender_ipv4, + mac: sender_mac, + hostname: None, + vendor: None + }); + } + } + + // For each target found, enhance each item with additional results + // results such as the hostname & MAC vendor. + let target_details = discover_map.into_values().map(|mut target_detail| { + + if options.resolve_hostname { + target_detail.hostname = find_hostname(target_detail.ipv4); + } + + if vendor_list.has_vendor_db() { + target_detail.vendor = vendor_list.search_by_mac(&target_detail.mac); + } + + target_detail + + }).collect(); + + // The response summary can be used to display analytics related to the + // performed ARP scans (packet counts, timings, ...) + let response_summary = ResponseSummary { + packet_count, + arp_count, + duration_ms: start_recording.elapsed().as_millis() + }; + (response_summary, target_details) +} + +/** + * Find the local hostname linked to an IPv4 address. This will perform a + * reverse DNS request in the local network to find the IPv4 hostname. + */ +fn find_hostname(ipv4: Ipv4Addr) -> Option { + + let ip: IpAddr = ipv4.into(); + match lookup_addr(&ip) { + Ok(hostname) => { + + // The 'lookup_addr' function returns an IP address if no hostname + // was found. If this is the case, we prefer switching to None. + if hostname.parse::().is_ok() { + return None; + } + + Some(hostname) + }, + Err(_) => None + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + use ipnetwork::Ipv4Network; + use std::env; + + #[test] + fn should_resolve_public_ip() { + + // Sometimes, we do not have access to public networks in the test + // environment and can pass the OFFLINE environment variable. + if env::var("OFFLINE").is_ok() { + assert_eq!(true, true); + } + else { + let ipv4 = Ipv4Addr::new(1,1,1,1); + assert_eq!(find_hostname(ipv4), Some("one.one.one.one".to_string())); + } + } + + #[test] + fn should_resolve_localhost() { + + let ipv4 = Ipv4Addr::new(127,0,0,1); + + assert_eq!(find_hostname(ipv4), Some("localhost".to_string())); + } + + #[test] + fn should_not_resolve_unknown_ip() { + + let ipv4 = Ipv4Addr::new(10,254,254,254); + + assert_eq!(find_hostname(ipv4), None); + } + + #[test] + fn should_iterate_over_empty_networks() { + + let mut iterator = NetworkIterator::new(&vec![], false); + + assert_eq!(iterator.next(), None); + } + + #[test] + fn should_iterate_over_single_address() { + + let network_a = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 32).unwrap() + ); + let target_network: Vec<&IpNetwork> = vec![ + &network_a + ]; + + let mut iterator = NetworkIterator::new(&target_network, false); + + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))); + assert_eq!(iterator.next(), None); + } + + #[test] + fn should_iterate_over_multiple_address() { + + let network_a = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 24).unwrap() + ); + let target_network: Vec<&IpNetwork> = vec![ + &network_a + ]; + + let mut iterator = NetworkIterator::new(&target_network, false); + + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 0)))); + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))); + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)))); + } + + #[test] + fn should_iterate_over_multiple_networks() { + + let network_a = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 32).unwrap() + ); + let network_b = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(10, 10, 20, 20), 32).unwrap() + ); + let target_network: Vec<&IpNetwork> = vec![ + &network_a, + &network_b + ]; + + let mut iterator = NetworkIterator::new(&target_network, false); + + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)))); + assert_eq!(iterator.next(), Some(IpAddr::V4(Ipv4Addr::new(10, 10, 20, 20)))); + assert_eq!(iterator.next(), None); + } + + #[test] + fn should_iterate_with_random() { + + let network_a = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(192, 168, 1, 1), 32).unwrap() + ); + let network_b = IpNetwork::V4( + Ipv4Network::new(Ipv4Addr::new(10, 10, 20, 20), 32).unwrap() + ); + let target_network: Vec<&IpNetwork> = vec![ + &network_a, + &network_b + ]; + + let mut iterator = NetworkIterator::new(&target_network, true); + + assert_eq!(iterator.next().is_some(), true); + assert_eq!(iterator.next().is_some(), true); + assert_eq!(iterator.next(), None); + } + +} diff --git a/src/time.rs b/src/time.rs new file mode 100644 index 0000000..ae662ef --- /dev/null +++ b/src/time.rs @@ -0,0 +1,145 @@ +/** + * Parse a given time string into milliseconds. This can be used to convert a + * string such as '20ms', '10s' or '1h' into adequate milliseconds. Without + * suffix, the default behavior is to parse into milliseconds. + */ +pub fn parse_to_milliseconds(time_arg: &str) -> Result { + + let len = time_arg.len(); + + if time_arg.ends_with("ms") { + let milliseconds_text = &time_arg[0..len-2]; + return match milliseconds_text.parse::() { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid milliseconds") + }; + } + + if time_arg.ends_with('s') { + let seconds_text = &time_arg[0..len-1]; + return match seconds_text.parse::().map(|value| value * 1000) { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid seconds") + }; + } + + if time_arg.ends_with('m') { + let seconds_text = &time_arg[0..len-1]; + return match seconds_text.parse::().map(|value| value * 1000 * 60) { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid minutes") + }; + } + + if time_arg.ends_with('h') { + let hour_text = &time_arg[0..len-1]; + return match hour_text.parse::().map(|value| value * 1000 * 60 * 60) { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid hours") + }; + } + + match time_arg.parse::() { + Ok(ms_value) => Ok(ms_value), + Err(_) => Err("invalid milliseconds") + } +} + +/** + * Format milliseconds to a human-readable string. This will of course give an + * approximation, but will be readable. + */ +pub fn format_milliseconds(milliseconds: u128) -> String { + + if milliseconds < 1000 { + return format!("{}ms", milliseconds); + } + + if milliseconds < 1000*60 { + let seconds = milliseconds / 1000; + return format!("{}s", seconds); + } + + if milliseconds < 1000*60*60 { + let minutes = milliseconds / 1000 / 60; + return format!("{}m", minutes); + } + + let hours: u128 = milliseconds / 1000 / 60 / 60; + format!("{}h", hours) +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn should_parse_milliseconds() { + + assert_eq!(parse_to_milliseconds("1000"), Ok(1000)); + } + + #[test] + fn should_parse_seconds() { + + assert_eq!(parse_to_milliseconds("5s"), Ok(5000)); + } + + #[test] + fn should_parse_minutes() { + + assert_eq!(parse_to_milliseconds("3m"), Ok(180_000)); + } + + #[test] + fn should_parse_hours() { + + assert_eq!(parse_to_milliseconds("2h"), Ok(7_200_000)); + } + + #[test] + fn should_deny_negative() { + + assert_eq!(parse_to_milliseconds("-45"), Err("invalid milliseconds")); + } + + #[test] + fn should_deny_floating_numbers() { + + assert_eq!(parse_to_milliseconds("3.235"), Err("invalid milliseconds")); + } + + #[test] + fn should_deny_invalid_characters() { + + assert_eq!(parse_to_milliseconds("3z"), Err("invalid milliseconds")); + } + + // --- + + #[test] + fn should_display_milliseconds() { + + assert_eq!(format_milliseconds(500), "500ms".to_string()); + } + + #[test] + fn should_display_seconds() { + + assert_eq!(format_milliseconds(2500), "2s".to_string()); + } + + #[test] + fn should_display_minutes() { + + assert_eq!(format_milliseconds(300_000), "5m".to_string()); + } + + #[test] + fn should_display_hours() { + + assert_eq!(format_milliseconds(4_200_000), "1h".to_string()); + } + +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..0189b7f --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,331 @@ +use std::process; +use std::sync::Arc; + +use pnet_datalink::NetworkInterface; +use ipnetwork::{IpNetwork, NetworkSize}; +use serde::Serialize; +use ansi_term::Color::{Green, Red}; + +use crate::network::{ResponseSummary, TargetDetails}; +use crate::args::ScanOptions; + +/** + * Prints on stdout a list of all available network interfaces with some + * technical details. The goal is to present the most useful technical details + * to pick the right network interface for scans. + */ +pub fn show_interfaces(interfaces: &[NetworkInterface]) { + + let mut interface_count = 0; + let mut ready_count = 0; + + println!(); + for interface in interfaces.iter() { + + let up_text = match interface.is_up() { + true => format!("{} UP", Green.paint("✔")), + false => format!("{} DOWN", Red.paint("✖")) + }; + let mac_text = match interface.mac { + Some(mac_address) => format!("{}", mac_address), + None => "No MAC address".to_string() + }; + let first_ip = match interface.ips.get(0) { + Some(ip_address) => format!("{}", ip_address), + None => "".to_string() + }; + + println!("{: <20} {: <18} {: <20} {}", interface.name, up_text, mac_text, first_ip); + + interface_count += 1; + if interface.is_up() && !interface.is_loopback() && !interface.ips.is_empty() { + ready_count += 1; + } + } + + println!(); + println!("Found {} network interfaces, {} seems ready for ARP scans", interface_count, ready_count); + if let Some(default_interface) = select_default_interface(interfaces) { + println!("Default network interface will be {}", default_interface.name); + } + println!(); +} + +pub fn print_ascii_packet() { + + println!(); + println!(" 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 "); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!("| Hardware type | Protocol type |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|"); + println!("| Hlen | Plen | Operation |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!("| Sender HA |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!("| Sender HA | Sender IP |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|"); + println!("| Sender IP | Target HA |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|"); + println!("| Target HA |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!("| Target IP |"); + println!("+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+"); + println!(); + println!(" - Hardware type (2 bytes), use --hw-type option to change"); + println!(" - Protocol type (2 bytes), use --proto-type option to change"); + println!(); +} + +/** + * Find a default network interface for scans, based on the operating system + * priority and some interface technical details. + */ +pub fn select_default_interface(interfaces: &[NetworkInterface]) -> Option { + + let default_interface = interfaces.iter().find(|interface| { + + if interface.mac.is_none() { + return false; + } + + if interface.ips.is_empty() || !interface.is_up() || interface.is_loopback() { + return false; + } + + let potential_ipv4 = interface.ips.iter().find(|ip| ip.is_ipv4()); + if potential_ipv4.is_none() { + return false; + } + + true + }); + + default_interface.cloned() +} + +/** + * Display scan settings before launching an ARP scan. This includes network + * details (IP range, interface, ...) and timing informations. + */ +pub fn display_prescan_details(ip_networks: &[&IpNetwork], selected_interface: &NetworkInterface, scan_options: Arc) { + + let mut network_list = ip_networks.iter().take(5).map(|network| network.to_string()).collect::>().join(", "); + if ip_networks.len() > 5 { + let more_text = format!(" ({} more)", ip_networks.len()-5); + network_list.push_str(&more_text); + } + + println!(); + println!("Selected interface {} with IP {}", selected_interface.name, network_list); + if let Some(forced_source_ipv4) = scan_options.source_ipv4 { + println!("The ARP source IPv4 will be forced to {}", forced_source_ipv4); + } + if let Some(forced_destination_mac) = scan_options.destination_mac { + println!("The ARP destination MAC will be forced to {}", forced_destination_mac); + } +} + +/** + * Computes multiple IPv4 networks total size, IPv6 network are not being + * supported by this function. + */ +pub fn compute_network_size(ip_networks: &[&IpNetwork]) -> u128 { + + ip_networks.iter().fold(0u128, |total_size, ip_network| { + + let network_size: u128 = match ip_network.size() { + NetworkSize::V4(ipv4_network_size) => ipv4_network_size.into(), + NetworkSize::V6(_) => { + eprintln!("IPv6 networks are not supported by the ARP protocol"); + process::exit(1); + } + }; + total_size + network_size + }) +} + +/** + * Display the scan results on stdout with a table. The 'final_result' vector + * contains all items that will be displayed. + */ +pub fn display_scan_results(response_summary: ResponseSummary, mut target_details: Vec, options: &ScanOptions) { + + target_details.sort_by_key(|item| item.ipv4); + + let mut hostname_len = 15; + let mut vendor_len = 15; + for detail in target_details.iter() { + + if let Some(hostname) = &detail.hostname { + if hostname.len() > hostname_len { + hostname_len = hostname.len(); + } + } + + if let Some(vendor) = &detail.vendor { + if vendor.len() > vendor_len { + vendor_len = vendor.len(); + } + } + } + + if !target_details.is_empty() { + println!(); + println!("| IPv4 | MAC | {: hostname, + None if !options.resolve_hostname => "(disabled)", + None => "" + }; + let vendor: &str = match &detail.vendor { + Some(vendor) => vendor, + None => "" + }; + println!("| {: <15} | {: <18} | {: print!("{}", Red.paint("no hosts found")), + 1 => print!("1 host found"), + _ => print!("{} hosts found", target_count) + } + let seconds_duration = (response_summary.duration_ms as f32) / (1000_f32); + println!(" in {:.3} seconds", seconds_duration); + + match response_summary.packet_count { + 0 => print!("No packets received, "), + 1 => print!("1 packet received, "), + _ => print!("{} packets received, ", response_summary.packet_count) + }; + match response_summary.arp_count { + 0 => println!("no ARP packets filtered"), + 1 => println!("1 ARP packet filtered"), + _ => println!("{} ARP packets filtered", response_summary.arp_count) + }; + println!(); +} + +#[derive(Serialize)] +struct SerializableResultItem { + ipv4: String, + mac: String, + hostname: String, + vendor: String +} + +#[derive(Serialize)] +struct SerializableGlobalResult { + packet_count: usize, + arp_count: usize, + duration_ms: u128, + results: Vec +} + +/** + * Transforms an ARP scan result (including KPI and target details) to a structure + * that can be serialized for export (JSON, YAML, CSV, ...) + */ +fn get_serializable_result(response_summary: ResponseSummary, target_details: Vec) -> SerializableGlobalResult { + + let exportable_results: Vec = target_details.into_iter() + .map(|detail| { + + let hostname = match &detail.hostname { + Some(hostname) => hostname.clone(), + None => String::from("") + }; + + let vendor = match &detail.vendor { + Some(vendor) => vendor.clone(), + None => String::from("") + }; + + SerializableResultItem { + ipv4: format!("{}", detail.ipv4), + mac: format!("{}", detail.mac), + hostname, + vendor + } + }) + .collect(); + + SerializableGlobalResult { + packet_count: response_summary.packet_count, + arp_count: response_summary.arp_count, + duration_ms: response_summary.duration_ms, + results: exportable_results + } +} + +/** + * Export the scan results as a JSON string with response details (timings, ...) + * and ARP results from the local network. + */ +pub fn export_to_json(response_summary: ResponseSummary, mut target_details: Vec) -> String { + + target_details.sort_by_key(|item| item.ipv4); + + let global_result = get_serializable_result(response_summary, target_details); + + serde_json::to_string(&global_result).unwrap_or_else(|err| { + eprintln!("Could not export JSON results ({})", err); + process::exit(1); + }) +} + +/** + * Export the scan results as a YAML string with response details (timings, ...) + * and ARP results from the local network. + */ +pub fn export_to_yaml(response_summary: ResponseSummary, mut target_details: Vec) -> String { + + target_details.sort_by_key(|item| item.ipv4); + + let global_result = get_serializable_result(response_summary, target_details); + + serde_yaml::to_string(&global_result).unwrap_or_else(|err| { + eprintln!("Could not export YAML results ({})", err); + process::exit(1); + }) +} + +/** + * Export the scan results as a CSV string with response details (timings, ...) + * and ARP results from the local network. + */ +pub fn export_to_csv(response_summary: ResponseSummary, mut target_details: Vec) -> String { + + target_details.sort_by_key(|item| item.ipv4); + + let global_result = get_serializable_result(response_summary, target_details); + + let mut wtr = csv::Writer::from_writer(vec![]); + + for result in global_result.results { + wtr.serialize(result).unwrap_or_else(|err| { + eprintln!("Could not serialize result to CSV ({})", err); + process::exit(1); + }); + } + wtr.flush().unwrap_or_else(|err| { + eprintln!("Could not flush CSV writer buffer ({})", err); + process::exit(1); + }); + + let convert_writer = wtr.into_inner().unwrap_or_else(|err| { + eprintln!("Could not convert final CSV result ({})", err); + process::exit(1); + }); + String::from_utf8(convert_writer).unwrap_or_else(|err| { + eprintln!("Could not convert final CSV result to text ({})", err); + process::exit(1); + }) +} diff --git a/src/vendor.rs b/src/vendor.rs new file mode 100644 index 0000000..27c9742 --- /dev/null +++ b/src/vendor.rs @@ -0,0 +1,145 @@ +use std::fs::File; +use std::process; + +use pnet_datalink::MacAddr; +use csv::{Position, Reader}; + +// The Vendor structure performs search operations on a vendor database to find +// which MAC address belongs to a specific vendor. All network vendors have a +// dedicated MAC address range that is registered by the IEEE and maintained in +// the OUI database. An OUI is a 24-bit globally unique assigned number +// referenced by various standards. +pub struct Vendor { + reader: Option>, +} + +impl Vendor { + + // Create a new MAC vendor search instance based on the given datebase path + // (absolute or relative). A failure will not throw an error, but leave the + // vendor search instance without database reader. + pub fn new(path: &str) -> Self { + + let file_result = File::open(path); + + match file_result { + Ok(file) => Vendor { + reader: Some(Reader::from_reader(file)), + }, + Err(_) => Vendor { + reader: None, + } + } + } + + pub fn has_vendor_db(&self) -> bool { + self.reader.is_some() + } + + // Find a vendor name based on a given MAC address. A vendor search + // operation will perform a whole read on the database for now. + pub fn search_by_mac(&mut self, mac_address: &MacAddr) -> Option { + + match &mut self.reader { + Some(reader) => { + + // The {:02X} syntax forces to pad all numbers with zero values. + // This ensures that a MAC 002272... will not be printed as + // 02272 and therefore fails the search process. + let vendor_oui = format!("{:02X}{:02X}{:02X}", mac_address.0, mac_address.1, mac_address.2); + + // Since we share a common instance of the CSV reader, it must be reset + // before each read (internal buffers will be cleared). + reader.seek(Position::new()).unwrap_or_else(|err| { + eprintln!("Could not reset the CSV reader ({})", err); + process::exit(1); + }); + + for vendor_result in reader.records() { + + let record = vendor_result.unwrap_or_else(|err| { + eprintln!("Could not read CSV record ({})", err); + process::exit(1); + }); + let potential_oui = record.get(1).unwrap_or(""); + + if vendor_oui.eq(potential_oui) { + return Some(record.get(2).unwrap_or("(no vendor)").to_string()) + } + } + + None + } + None => None + } + } + +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn should_create_vendor_resolver() { + + let vendor = Vendor::new("./data/ieee-oui.csv"); + + assert_eq!(vendor.has_vendor_db(), true); + } + + #[test] + fn should_handle_unresolved_database() { + + let vendor = Vendor::new("./unknown.csv"); + + assert_eq!(vendor.has_vendor_db(), false); + } + + #[test] + fn should_find_specific_mac_vendor() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0x40, 0x55, 0x82, 0xc3, 0xe5, 0x5b); + + assert_eq!(vendor.search_by_mac(&mac), Some("Nokia".to_string())); + } + + #[test] + fn should_find_first_mac_vendor() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0x00, 0x22, 0x72, 0xd7, 0xb5, 0x23); + + assert_eq!(vendor.search_by_mac(&mac), Some("American Micro-Fuel Device Corp.".to_string())); + } + + #[test] + fn should_find_last_mac_vendor() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0xcc, 0x9d, 0xa2, 0x14, 0x2e, 0x6f); + + assert_eq!(vendor.search_by_mac(&mac), Some("Eltex Enterprise Ltd.".to_string())); + } + + #[test] + fn should_handle_unknown_mac_vendor() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0xbb, 0xbb, 0xbb, 0xd2, 0xf5, 0xb6); + + assert_eq!(vendor.search_by_mac(&mac), None); + } + + #[test] + fn should_pad_correctly_with_zeroes() { + + let mut vendor = Vendor::new("./data/ieee-oui.csv"); + let mac = MacAddr::new(0x01, 0x01, 0x01, 0x67, 0xb2, 0x1d); + + assert_eq!(vendor.search_by_mac(&mac), Some("SomeCorp".to_string())); + } + +}