diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..149a1e5
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,22 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+version: 2
+updates:
+- package-ecosystem: cargo
+ directory: "/"
+ schedule:
+ interval: daily
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..62cf039
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,120 @@
+# This file is part of masscanned.
+# Copyright 2021 - 2024 The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+name: Build masscanned
+
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Git checkout
+ uses: actions/checkout@v2
+
+ - name: Get Rust toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+
+ - name: Run cargo fmt
+ uses: actions-rs/cargo@v1
+ with:
+ command: fmt
+ args: -- --check
+
+ - name: Install packages for build
+ run: sudo apt-get -q update && sudo apt-get -qy install libpcap-dev
+
+ - name: Run cargo build
+ uses: actions-rs/cargo@v1
+ with:
+ command: build
+
+ - name: Run cargo test
+ uses: actions-rs/cargo@v1
+ with:
+ command: test
+
+ - name: Create build archive
+ run: tar cf masscanned.tar target/debug/masscanned
+
+ - name: Upload binary
+ uses: actions/upload-artifact@v4
+ with:
+ name: masscanned.tar
+ path: masscanned.tar
+
+ test:
+ needs: build
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Git checkout
+ uses: actions/checkout@v2
+
+ - name: Get binary
+ uses: actions/download-artifact@v4
+ with:
+ name: masscanned.tar
+
+ - name: Extract build archive
+ run: tar xf masscanned.tar
+
+ - name: Use Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.9
+
+ - name: Install dependencies
+ run: sudo pip install -U -r test/requirements.txt
+
+ - name: Install linting tools
+ run: sudo pip install -U flake8 black
+
+ - name: Install packages for tests
+ run: sudo apt-get -q update && sudo apt-get -qy install nmap rpcbind smbclient
+
+ - name: Run black
+ run: black -t py36 --check test/test_masscanned.py test/src/
+
+ - name: Run flake8
+ run: flake8 --ignore=E266,E501,W503 test/test_masscanned.py test/src/
+
+ - name: Run tests
+ run: sudo python test/test_masscanned.py
+
+ - name: Display logs
+ run: echo STDOUT; cat test/res/masscanned.stdout && echo && echo STDERR && cat test/res/masscanned.stderr
+ if: failure()
+
+ docker:
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Git checkout
+ uses: actions/checkout@v2
+
+ - name: Build archive
+ run: git archive --format=tar --prefix=masscanned-master/ HEAD -o docker/masscanned.tar
+
+ - name: Build image
+ uses: docker/build-push-action@v5
+ with:
+ push: false
+ context: docker/
+ file: docker/Dockerfile-local
diff --git a/.gitignore b/.gitignore
index f07cd65..b40b324 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,11 @@
/target/
-Cargo.lock
**/*.rs.bk
# Vim temporary files
*.swp
*.swo
+# Emacs temporary files
+*~
*__pycache__*
test/res/*
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..30cd324
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1139 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "aho-corasick"
+version = "0.7.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd2405b3ac1faab2990b74d728624cd9fd115651fcecc7c2d8daf01376275ba"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+
+[[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 = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
+
+[[package]]
+name = "bumpalo"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder_slice"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b294e30387378958e8bf8f4242131b930ea615ff81e8cac2440cea0a6013190"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eac00902d9d136acd712710d71823fb8ac8004ca445a89e73a41d45aa712931"
+dependencies = [
+ "clap_builder",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ad9bbf750e73b5884fb8a211a9424a1906c1e156724260fdae972f31d70e1d6"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+
+[[package]]
+name = "codespan-reporting"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "cxx"
+version = "1.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90d59d9acd2a682b4e40605a242f6670eaa58c5957471cbf85e8aa6a0b97a5e8"
+dependencies = [
+ "cc",
+ "cxxbridge-flags",
+ "cxxbridge-macro",
+ "link-cplusplus",
+]
+
+[[package]]
+name = "cxx-build"
+version = "1.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebfa40bda659dd5c864e65f4c9a2b0aff19bea56b017b9b77c73d3766a453a38"
+dependencies = [
+ "cc",
+ "codespan-reporting",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "scratch",
+ "syn 1.0.107",
+]
+
+[[package]]
+name = "cxxbridge-flags"
+version = "1.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "457ce6757c5c70dc6ecdbda6925b958aae7f959bda7d8fb9bde889e34a09dc03"
+
+[[package]]
+name = "cxxbridge-macro"
+version = "1.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebf883b7aacd7b2aeb2a7b338648ee19f57c140d4ee8e52c68979c6b2f7f2263"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.107",
+]
+
+[[package]]
+name = "derive-into-owned"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d94d81e3819a7b06a8638f448bc6339371ca9b6076a99d4a43eece3c4c923"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.107",
+]
+
+[[package]]
+name = "dns-parser"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4d33be9473d06f75f58220f71f7a9317aca647dc061dbd3c361b0bef505fbea"
+dependencies = [
+ "byteorder",
+ "quick-error",
+]
+
+[[package]]
+name = "either"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
+
+[[package]]
+name = "errno"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.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 = "flate2"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasi",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "winapi",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
+dependencies = [
+ "cxx",
+ "cxx-build",
+]
+
+[[package]]
+name = "ipnetwork"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455"
+dependencies = [
+ "hermit-abi",
+ "rustix",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.172"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
+
+[[package]]
+name = "libloading"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.52.0",
+]
+
+[[package]]
+name = "link-cplusplus"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "log"
+version = "0.4.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
+
+[[package]]
+name = "masscanned"
+version = "0.2.0"
+dependencies = [
+ "bitflags 2.9.4",
+ "byteorder",
+ "chrono",
+ "clap",
+ "dns-parser",
+ "flate2",
+ "itertools",
+ "lazy_static",
+ "log",
+ "pcap",
+ "pcap-file",
+ "pnet",
+ "rand",
+ "siphasher",
+ "stderrlog",
+ "strum",
+ "strum_macros",
+]
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "no-std-net"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65"
+
+[[package]]
+name = "num-traits"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+
+[[package]]
+name = "pcap"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83cdabc34a80d9ec3563694cc31423fba6bb9bab4f31a9a5d5b85f29bd6d660a"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno 0.2.8",
+ "libc",
+ "libloading",
+ "pkg-config",
+ "regex",
+ "windows-sys 0.36.1",
+]
+
+[[package]]
+name = "pcap-file"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fc1f139757b058f9f37b76c48501799d12c9aa0aa4c0d4c980b062ee925d1b2"
+dependencies = [
+ "byteorder_slice",
+ "derive-into-owned",
+ "thiserror",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
+
+[[package]]
+name = "pnet"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd959a8268165518e2bf5546ba84c7b3222744435616381df3c456fe8d983576"
+dependencies = [
+ "ipnetwork",
+ "pnet_base",
+ "pnet_datalink",
+ "pnet_packet",
+ "pnet_sys",
+ "pnet_transport",
+]
+
+[[package]]
+name = "pnet_base"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872e46346144ebf35219ccaa64b1dffacd9c6f188cd7d012bd6977a2a838f42e"
+dependencies = [
+ "no-std-net",
+]
+
+[[package]]
+name = "pnet_datalink"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c302da22118d2793c312a35fb3da6846cb0fab6c3ad53fd67e37809b06cdafce"
+dependencies = [
+ "ipnetwork",
+ "libc",
+ "pnet_base",
+ "pnet_sys",
+ "winapi",
+]
+
+[[package]]
+name = "pnet_macros"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a780e80005c2e463ec25a6e9f928630049a10b43945fea83207207d4a7606f4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 1.0.107",
+]
+
+[[package]]
+name = "pnet_macros_support"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d932134f32efd7834eb8b16d42418dac87086347d1bc7d142370ef078582bc"
+dependencies = [
+ "pnet_base",
+]
+
+[[package]]
+name = "pnet_packet"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bde678bbd85cb1c2d99dc9fc596e57f03aa725f84f3168b0eaf33eeccb41706"
+dependencies = [
+ "glob",
+ "pnet_base",
+ "pnet_macros",
+ "pnet_macros_support",
+]
+
+[[package]]
+name = "pnet_sys"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf7a58b2803d818a374be9278a1fe8f88fce14b936afbe225000cfcd9c73f16"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "pnet_transport"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "813d1c0e4defbe7ee22f6fe1755f122b77bfb5abe77145b1b5baaf463cab9249"
+dependencies = [
+ "libc",
+ "pnet_base",
+ "pnet_packet",
+ "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.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "regex"
+version = "1.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
+
+[[package]]
+name = "rustix"
+version = "0.38.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
+dependencies = [
+ "bitflags 2.9.4",
+ "errno 0.3.8",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "scratch"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2"
+
+[[package]]
+name = "serde"
+version = "1.0.152"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
+
+[[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
+name = "stderrlog"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b"
+dependencies = [
+ "chrono",
+ "is-terminal",
+ "log",
+ "termcolor",
+ "thread_local",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+
+[[package]]
+name = "strum"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
+
+[[package]]
+name = "strum_macros"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.18",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.107",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "wasi"
+version = "0.14.2+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.107",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.107",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+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-link"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
+
+[[package]]
+name = "windows-sys"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+dependencies = [
+ "windows_aarch64_msvc 0.36.1",
+ "windows_i686_gnu 0.36.1",
+ "windows_i686_msvc 0.36.1",
+ "windows_x86_64_gnu 0.36.1",
+ "windows_x86_64_msvc 0.36.1",
+]
+
+[[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-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.0",
+]
+
+[[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-targets"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.0",
+ "windows_aarch64_msvc 0.52.0",
+ "windows_i686_gnu 0.52.0",
+ "windows_i686_msvc 0.52.0",
+ "windows_x86_64_gnu 0.52.0",
+ "windows_x86_64_gnullvm 0.52.0",
+ "windows_x86_64_msvc 0.52.0",
+]
+
+[[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_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+
+[[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_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+
+[[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_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+
+[[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_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.36.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
+dependencies = [
+ "bitflags 2.9.4",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 789f87f..6cd59b7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,22 +21,23 @@ authors = ["_Frky <3105926+Frky@users.noreply.github.com>"]
edition = "2018"
[dependencies]
-pcap = "0.7.0"
-pcap-file = "1.1.1"
-pnet = "0.26.0"
-# pnet = { path = "libpnet" }
-clap = "2.33.3"
-log = "0.4.11"
-stderrlog = "0.5.0"
-itertools = "0.9.0"
-rand = "0.7.3"
+bitflags = "2.9.4"
+byteorder = "1.5.0"
+chrono = "0.4.42"
+clap = "4.5.47"
dns-parser = "0.8.0"
-netdevice = "0.1.1"
-bitflags = "1.2.1"
-lazy_static = "1.4.0"
-siphasher = "0.3"
-chrono = "0.4.19"
-byteorder = "1.4.3"
+flate2 = "1.1"
+itertools = "0.14.0"
+lazy_static = "1.5.0"
+log = "0.4.28"
+pcap = "2.3.0"
+pcap-file = "2.0.0"
+pnet = { version = "0.33.0", features = ["std"] }
+rand = "0.9.2"
+siphasher = "1.0"
+stderrlog = "0.6.0"
+strum = "0.27.2"
+strum_macros = "0.27.2"
[[bin]]
name = "masscanned"
diff --git a/README.md b/README.md
index 8f5a1f6..d61c5f9 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,10 @@
+[](https://github.com/ivre/masscanned/actions/workflows/test.yml?branch=master)
+
# Masscanned
**Masscanned** (name inspired, of course, by [masscan](https://github.com/robertdavidgraham/masscan))
is a network responder. Its purpose is to provide generic answers to as many protocols as possible,
-and with as few asumptions as possible on the client's intentions.
+and with as few assumptions as possible on the client's intentions.
> *Let them talk first.*
@@ -17,22 +19,32 @@ For example, when it receives network packets:
* **masscanned** answers to `TCP SYN` (any port) with `TCP SYN/ACK` on any port,
* **masscanned** answers to `HTTP` requests (any verb) over `TCP/UDP` (any port) with a `HTTP 401` web page.
-
+
+
+## Overview
**Masscanned** currently supports most common protocols at layers 2-3-4, and a few application
-protocols:
+protocols.
-* `Eth::ARP::REQ`,
-* `Eth::IPv{4,6}::ICMP::ECHO-REQ`,
-* `Eth::IPv{4,6}::TCP::SYN` (all ports),
-* `Eth::IPv{4,6}::TCP::PSHACK` (all ports),
-* `Eth::IPv6::ICMP::ND_NS`.
-* `Eth::IPv{4,6}::{TCP,UDP}::HTTP` (all HTTP verbs),
-* `Eth::IPv{4,6}::{TCP,UDP}::STUN`,
-* `Eth::IPv{4,6}::{TCP,UDP}::SSH` (Server Protocol only).
+### Network protocols
+
+* ARP (answers to ARP requests)
+* ICMP (answers to ping)
+* ICMPv6 (answers to ND NS)
+* TCP (answers to SYN and PUSH)
+
+### Application protocols
+
+* HTTP (answers to all verbs)
+* SSH (answers to the client banner)
+* STUN (answers to binding requests)
+* SMB
+* DNS (answers to IN/A queries)
## Try it locally
+### On your host
+
1. Build **masscanned**
```
$ cargo build
@@ -64,13 +76,77 @@ $ cargo build
...
```
-## Protocols
+### In a Docker
+
+1. Install docker:
+```
+# apt install docker.io
+```
+1. Build docker container:
+```
+$ cd masscanned/docker && docker build -t masscanned:test .
+```
+1. Run docker container:
+```
+$ docker run --cap-add=NET_ADMIN masscanned:test
+```
+1. Send packets to **masscanned**
+```
+# arping 172.17.0.2
+# ping 172.17.0.2
+# nc -n -v 172.17.0.2 80
+# nc -n -v -u 172.17.0.2 80
+...
+```
+
+## Use it
+
+A good use of **masscanned** is to deploy it on a VPS with one or more public IP addresses.
+
+To use the results, the best way is to capture all network traffic on the interface **masscanned** is listening to/responding on.
+The pcaps can then be analyzed using [zeek](https://zeek.org/) and the output files can typically be pushed in an instance of **IVRE**.
+
+A documentation on how to deploy an instance of **masscanned** on a VPS is coming (see [Issue #2](https://github.com/ivre/masscanned/issues/2)).
+
+### Supported options
+
+```
+Network answering machine for various network protocols (L2-L3-L4 + applications)
+
+Usage: masscanned [OPTIONS] --iface
+
+Options:
+ -i, --iface
+ the interface to use for receiving/sending packets
+ -m, --mac-addr
+ MAC address to use in the response packets
+ --self-ip-file
+ File with the list of IP addresses handled by masscanned
+ --self-ip-list
+ Inline list of IP addresses handled by masscanned, comma-separated
+ --remote-ip-deny-file
+ File with the list of IP addresses from which masscanned will ignore packets
+ --remote-ip-deny-list
+ Inline list of IP addresses from which masscanned will ignore packets
+ -v...
+ Increase message verbosity
+ -q, --quiet
+ Quiet mode: do not output anything on stdout
+ --format
+ Format in which to output logs [default: console] [possible values: console, logfmt]
+ -h, --help
+ Print help information
+ -V, --version
+ Print version information
+```
+
+## Supported protocols - details
### Layer 2
#### ARP
-`masscanned` anwsers to `ARP` requests, for requests that target an `IPv4` address
+`masscanned` answers to `ARP` requests, for requests that target an `IPv4` address
that is handled by `masscanned` (*i.e.*, an address that is in the
IP address file given with option `-f`).
@@ -112,7 +188,7 @@ An additionnal requirement is that the next layer protocol is supported - see be
#### IPv4
-The following L4 protocols are suppported for an `IPv4` packet:
+The following L3+/4 protocols are supported for an `IPv4` packet:
* `ICMPv4`
* `UDP`
@@ -122,7 +198,7 @@ If the next layer protocol is not one of them, the packet is dropped.
#### IPv6
-The following L4 protocols are suppported for an `IPv6` packet:
+The following L3+/4 protocols are supported for an `IPv6` packet:
* `ICMPv6`
* `UDP`
@@ -148,7 +224,7 @@ code `0` and the same payload as the incoming packet, as specified by [RFC 792](
* the `ICMP` type is `NeighborSol` (`135`) **and**:
* no IP (v4 or v6) was speficied for `masscanned`
- * **or** the target address of the Neighbor Solicitation is one of `masccanned`
+ * **or** the target address of the Neighbor Solicitation is one of `masscanned`
*In that case, the answer is a `Neighbor Advertisement` (`136`) packet with `masscanned` `MAC` address*
@@ -166,19 +242,68 @@ code `0` and the same payload as the incoming packet, as specified by [RFC 792](
a supported protocol (Layer 5/6/7) has been detected,
* if the received packet has flag `ACK`, it is ignored,
* if the received packet has flag `RST` or `FIN-ACK`, it is ignored,
-* if the received packet has flag `SYN`, then `masscanned` answers with a `SYN-ACK` packet, setting a **SYNACK-cookie** in the sequence number.
+* if the received packet has flag `SYN`, then `masscanned` tries to imitate the behaviour
+of a standard Linux stack - which is:
+ * if there are additional flags that are not among `PSH`, `URG`, `CWR`, `ECE`, then the `SYN` is ignored,
+ * if the flags `CWR` and`ECE` are simultaneously set, then the `SYN` is ignored,
+ * in any other case, `masscanned` answers with a `SYN-ACK` packet, setting a **SYNACK-cookie** in the sequence number.
#### UDP
`masscanned` answers to an `UDP` packet if and only if the upper-layer protocol
is handled and provides an answer.
-### Protocols
+### Application protocols
#### HTTP
+`masscanned` answers to any `HTTP` request (any **valid** verb) with a `401 Authorization Required`.
+Note that `HTTP` requests with an invalid verb will not be answered.
+
+Example:
+
+```
+$ curl -X GET 10.11.10.129
+
+401 Authorization Required
+
+401 Authorization Required
+
nginx/1.14.2
+
+
+$ curl -X OPTIONS 10.11.10.129
+
+401 Authorization Required
+
+401 Authorization Required
+
nginx/1.14.2
+
+
+$ curl -X HEAD 10.11.10.129
+Warning: Setting custom HTTP method to HEAD with -X/--request may not work the
+Warning: way you want. Consider using -I/--head instead.
+
+401 Authorization Required
+
+401 Authorization Required
+
nginx/1.14.2
+
+
+$ curl -X XXX 10.11.10.129
+[timeout]
+```
+
#### STUN
+Example:
+
+```
+$ stun 10.11.10.129
+STUN client version 0.97
+Primary: Open
+Return value is 0x000001
+```
+
#### SSH
`masscanned` answers to `SSH` `Client: Protocol` messages with the following `Server: Protocol` message:
@@ -187,6 +312,57 @@ is handled and provides an answer.
SSH-2.0-1\r\n
```
+#### SMB
+
+`masscanned` answers to `Negotiate Protocol Request` packets in order for the
+client to send a `NTLMSSP_NEGOTIATE`, to which `masscanned` answers with a challenge.
+
+Example:
+
+```
+##$ smbclient -U user \\\\10.11.10.129\\shared
+Enter WORKGROUP\user's password:
+```
+
+#### DNS
+
+`masscanned` answers to `DNS` queries of class `IN` and type `A` (for now).
+The answer it provides always contains the IP address the query was sent to.
+
+Example:
+
+```
+$ host -t A masscan.ned 10.11.10.129
+Using domain server:
+Name: 10.11.10.129
+Address: 10.11.10.129#53
+Aliases:
+
+masscan.ned has address 10.11.10.129
+$ host -t A masscan.ned 10.11.10.130
+Using domain server:
+Name: 10.11.10.130
+Address: 10.11.10.130#53
+Aliases:
+
+masscan.ned has address 10.11.10.130
+$ host -t A masscan.ned 10.11.10.131
+Using domain server:
+Name: 10.11.10.131
+Address: 10.11.10.131#53
+Aliases:
+
+masscan.ned has address 10.11.10.131
+$ host -t A masscan.ned 10.11.10.132
+Using domain server:
+Name: 10.11.10.132
+Address: 10.11.10.132#53
+Aliases:
+
+masscan.ned has address 10.11.10.132
+
+```
+
## Internals
### Tests
@@ -196,68 +372,118 @@ SSH-2.0-1\r\n
```
$ cargo test
Compiling masscanned v0.2.0 (/zdata/workdir/masscanned)
- Finished test [unoptimized + debuginfo] target(s) in 2.34s
- Running target/debug/deps/masscanned-b86211a090e50323
+ Finished test [unoptimized + debuginfo] target(s) in 3.83s
+ Running unittests (target/debug/deps/masscanned-f9292f8600038978)
-running 36 tests
+running 92 tests
test client::client_info::tests::test_client_info_eq ... ok
test layer_2::arp::tests::test_arp_reply ... ok
-test layer_3::ipv4::tests::test_ipv4_reply ... ok
-test layer_3::ipv6::tests::test_ipv6_reply ... ok
-test layer_4::icmpv6::tests::test_icmpv6_reply ... ok
+test layer_2::tests::test_eth_empty ... ok
test layer_2::tests::test_eth_reply ... ok
-test layer_4::icmpv6::tests::test_nd_na_reply ... ok
-test layer_4::tcp::tests::test_synack_cookie_ipv4 ... ok
+test layer_3::ipv4::tests::test_ipv4_reply ... ok
+test layer_3::ipv4::tests::test_ipv4_empty ... ok
+test layer_3::ipv6::tests::test_ipv6_empty ... ok
+test layer_3::ipv6::tests::test_ipv6_reply ... ok
test layer_4::icmpv4::tests::test_icmpv4_reply ... ok
+test layer_4::icmpv6::tests::test_icmpv6_reply ... ok
+test layer_4::icmpv6::tests::test_nd_na_reply ... ok
test layer_4::tcp::tests::test_synack_cookie_ipv6 ... ok
-test proto::http::test_http_request_field ... ok
-test proto::http::test_http_request_no_field ... ok
-test proto::http::test_http_request_line ... ok
-test proto::http::test_http_verb ... ok
-test proto::stun::tests::test_change_request_port ... ok
-test proto::stun::tests::test_proto_stun_ipv6 ... ok
-test proto::stun::tests::test_proto_stun_ipv4 ... ok
+test layer_4::tcp::tests::test_tcp_fin_ack_wrap ... ok
+test proto::dns::cst::tests::class_parse ... ok
+test layer_4::tcp::tests::test_tcp_fin_ack ... ok
+test layer_4::tcp::tests::test_synack_cookie_ipv4 ... ok
+test proto::dns::cst::tests::type_parse ... ok
+test proto::dns::header::tests::parse_byte_by_byte ... ok
+test proto::dns::header::tests::repl_id ... ok
+test proto::dns::header::tests::repl_opcode ... ok
+test proto::dns::header::tests::repl_ancount ... ok
+test proto::dns::header::tests::repl_rd ... ok
+test proto::dns::query::tests::parse_in_a_all ... ok
+test proto::dns::header::tests::parse_all ... ok
+test proto::dns::query::tests::repl ... ok
+test proto::dns::query::tests::reply_in_a ... ok
+test proto::dns::rr::tests::parse_all ... ok
+test proto::dns::rr::tests::parse_byte_by_byte ... ok
+test proto::dns::query::tests::parse_in_a_byte_by_byte ... ok
+test proto::dns::tests::parse_qd_all ... ok
+test proto::dns::tests::parse_qd_byte_by_byte ... ok
+test proto::dns::rr::tests::build ... ok
+test proto::dns::tests::parse_qd_rr_all ... ok
+test proto::dns::tests::parse_qr_rr_byte_by_byte ... ok
+test proto::dns::tests::parse_rr_byte_by_byte ... ok
+test proto::dns::tests::parse_rr_all ... ok
+test proto::dns::tests::reply_in_a ... ok
+test proto::http::tests::test_http_request_line ... ok
+test proto::http::tests::test_http_request_no_field ... ok
+test proto::http::tests::test_http_request_field ... ok
+test proto::http::tests::test_http_verb ... ok
+test proto::rpc::tests::test_probe_nmap ... ok
+test proto::rpc::tests::test_probe_nmap_split1 ... ok
+test proto::rpc::tests::test_probe_portmap_v4_dump ... ok
+test proto::rpc::tests::test_probe_nmap_split2 ... ok
+test proto::rpc::tests::test_probe_nmap_udp ... ok
+test proto::smb::tests::test_smb1_session_setup_request_parse ... ok
+test proto::smb::tests::test_smb1_protocol_nego_parsing ... ok
+test proto::smb::tests::test_smb1_protocol_nego_reply ... ok
+test proto::smb::tests::test_smb1_session_setup_request_reply ... ok
+test proto::smb::tests::test_smb2_protocol_nego_parsing ... ok
+test proto::smb::tests::test_smb2_protocol_nego_reply ... ok
+test proto::smb::tests::test_smb2_session_setup_request_reply ... ok
+test proto::smb::tests::test_smb2_session_setup_request_parse ... ok
+test proto::ssh::tests::ssh_1_banner_cr ... ok
+test proto::ssh::tests::ssh_1_banner_crlf ... ok
+test proto::ssh::tests::ssh_1_banner_lf ... ok
+test proto::ssh::tests::ssh_1_banner_space ... ok
+test proto::ssh::tests::ssh_2_banner_cr ... ok
+test proto::ssh::tests::ssh_1_banner_parse ... ok
+test proto::ssh::tests::ssh_2_banner_parse ... ok
+test proto::ssh::tests::ssh_2_banner_lf ... ok
+test proto::ssh::tests::ssh_2_banner_crlf ... ok
test proto::stun::tests::test_change_request_port_overflow ... ok
-test smack::smack::tests::test_anchor_end ... ok
-test smack::smack::tests::test_anchor_begin ... ok
-test smack::smack::tests::test_multiple_matches ... ok
-test smack::smack::tests::test_http_banner ... ok
-test smack::smack::tests::test_multiple_matches_wildcard ... ok
-test smack::smack::tests::test_proto ... ok
-test smack::smack::tests::test_wildcard ... ok
+test proto::stun::tests::test_proto_stun_ipv4 ... ok
+test proto::stun::tests::test_change_request_port ... ok
+test proto::ssh::tests::ssh_2_banner_space ... ok
+test proto::stun::tests::test_proto_stun_ipv6 ... ok
+test proto::tcb::tests::test_proto_tcb_proto_state_http ... ok
+test proto::tests::dispatch_dns ... ok
+test proto::tcb::tests::test_proto_tcb_proto_state_rpc ... ok
+test proto::tcb::tests::test_proto_tcb_proto_id ... ok
+test proto::tests::test_proto_dispatch_http ... ok
test proto::tests::test_proto_dispatch_ssh ... ok
+test proto::tests::test_proto_dispatch_ghost ... ok
test proto::tests::test_proto_dispatch_stun ... ok
+test smack::smack::tests::test_anchor_end ... ok
+test smack::smack::tests::test_multiple_matches_wildcard ... ok
+test smack::smack::tests::test_multiple_matches ... ok
+test smack::smack::tests::test_anchor_begin ... ok
+test smack::smack::tests::test_http_banner ... ok
test synackcookie::tests::test_clientinfo ... ok
+test synackcookie::tests::test_ip4 ... ok
test synackcookie::tests::test_ip4_dst ... ok
test synackcookie::tests::test_ip4_src ... ok
-test synackcookie::tests::test_ip4 ... ok
test synackcookie::tests::test_ip6 ... ok
test synackcookie::tests::test_key ... ok
test synackcookie::tests::test_tcp_dst ... ok
test synackcookie::tests::test_tcp_src ... ok
+test smack::smack::tests::test_wildcard ... ok
+test smack::smack::tests::test_proto ... ok
test smack::smack::tests::test_pattern ... ok
-test result: ok. 36 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
+test result: ok. 92 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.41s
```
#### Functional tests
```
# ./test/test_masscanned.py
-tcpdump: listening on tap0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
INFO test_arp_req......................................OK
INFO test_arp_req_other_ip.............................OK
-INFO test_ipv4_req.....................................OK
-INFO test_eth_req_other_mac............................OK
-INFO test_ipv4_req_other_ip............................OK
-INFO test_icmpv4_echo_req..............................OK
-INFO test_icmpv6_neighbor_solicitation.................OK
-INFO test_icmpv6_neighbor_solicitation_other_ip........OK
-INFO test_icmpv6_echo_req..............................OK
-INFO test_tcp_syn......................................OK
-INFO test_ipv4_tcp_psh_ack.............................OK
-INFO test_ipv6_tcp_psh_ack.............................OK
+INFO test_ipv4_udp_dns_in_a............................OK
+INFO test_ipv4_udp_dns_in_a_multiple_queries...........OK
+INFO test_ipv4_tcp_ghost...............................OK
INFO test_ipv4_tcp_http................................OK
+INFO test_ipv4_tcp_http_segmented......................OK
+INFO test_ipv4_tcp_http_incomplete.....................OK
INFO test_ipv6_tcp_http................................OK
INFO test_ipv4_udp_http................................OK
INFO test_ipv6_udp_http................................OK
@@ -265,26 +491,64 @@ INFO test_ipv4_tcp_http_ko.............................OK
INFO test_ipv4_udp_http_ko.............................OK
INFO test_ipv6_tcp_http_ko.............................OK
INFO test_ipv6_udp_http_ko.............................OK
-INFO test_ipv4_udp_stun................................OK
-INFO test_ipv6_udp_stun................................OK
-INFO test_ipv4_udp_stun_change_port....................OK
-INFO test_ipv6_udp_stun_change_port....................OK
+INFO test_icmpv4_echo_req..............................OK
+INFO test_icmpv6_neighbor_solicitation.................OK
+INFO test_icmpv6_neighbor_solicitation_other_ip........OK
+INFO test_icmpv6_echo_req..............................OK
+INFO test_ipv4_req.....................................OK
+INFO test_eth_req_other_mac............................OK
+INFO test_ipv4_req_other_ip............................OK
+INFO test_rpc_nmap.....................................OK
+INFO test_rpcinfo......................................OK
+INFO test_smb1_network_req.............................OK
+INFO test_smb2_network_req.............................OK
INFO test_ipv4_tcp_ssh.................................OK
INFO test_ipv4_udp_ssh.................................OK
INFO test_ipv6_tcp_ssh.................................OK
INFO test_ipv6_udp_ssh.................................OK
-tcpdump: pcap_loop: The interface disappeared
-604 packets captured
-604 packets received by filter
-0 packets dropped by kernel
+INFO test_ipv4_udp_stun................................OK
+INFO test_ipv6_udp_stun................................OK
+INFO test_ipv4_udp_stun_change_port....................OK
+INFO test_ipv6_udp_stun_change_port....................OK
+INFO test_ipv4_tcp_empty...............................OK
+INFO test_ipv6_tcp_empty...............................OK
+INFO test_tcp_syn......................................OK
+INFO test_ipv4_tcp_psh_ack.............................OK
+INFO test_ipv6_tcp_psh_ack.............................OK
+INFO test_ipv4_udp_empty...............................OK
+INFO test_ipv6_udp_empty...............................OK
+INFO Ran 41 tests with 0 errors
```
-### Logging Policy
+You can also chose what tests to run using the `TESTS` environment variable
+```
+TESTS=smb ./test/test_masscanned.py
+INFO test_smb1_network_req.............................OK
+INFO test_smb2_network_req.............................OK
+INFO Ran 2 tests with 0 errors
+```
-* `ERR`: any error - will always be displayed.
-* `WARN`, `-v`: responses sent by `masscanned`.
-* `INFO`, `-vv`: packets not handled, packets ignored.
-* `DEBUG`, `-vvv`: all packets received and sent by `masscanned`.
+## Logging
+
+### Console Logger
+
+**Verbs**:
+* `init`
+* `recv`
+* `send`
+* `drop`
+
+#### ARP
+
+```
+$ts arp $verb $operation $client_mac $client_ip $masscanned_mac $masscanned_ip
+```
+
+#### Ethernet
+
+```
+$ts eth $verb $ethertype $client_mac $masscanned_mac
+```
## To Do
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..5128596
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,19 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+SOURCEDIR = .
+BUILDDIR = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/doc/conf.py b/doc/conf.py
new file mode 100644
index 0000000..bbedcb5
--- /dev/null
+++ b/doc/conf.py
@@ -0,0 +1,53 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+from ast import literal_eval
+import configparser
+import os
+
+# -- Path setup --------------------------------------------------------------
+
+# -- Project information -----------------------------------------------------
+
+project = "IVRE"
+copyright = "2021, The IVRE project"
+html_logo = "img/logo.png"
+master_doc = "index"
+
+def parse_cargo():
+ config = configparser.ConfigParser()
+ config.read(os.path.join("..", "Cargo.toml"))
+ if "package" not in config:
+ return None, None, None
+ package = config["package"]
+ try:
+ author = literal_eval(package.get("authors"))[0].split("<", 1)[0].strip()
+ except KeyError:
+ authors = None
+ return literal_eval(package.get("name")), author, literal_eval(package.get("version"))
+
+project, author, version = parse_cargo()
+
+# -- General configuration ---------------------------------------------------
+
+extensions = []
+
+autosectionlabel_prefix_document = True
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = "sphinx_rtd_theme"
diff --git a/doc/demo.gif b/doc/img/demo.gif
similarity index 100%
rename from doc/demo.gif
rename to doc/img/demo.gif
diff --git a/doc/img/logo.png b/doc/img/logo.png
new file mode 100644
index 0000000..b9b5199
Binary files /dev/null and b/doc/img/logo.png differ
diff --git a/doc/index.rst b/doc/index.rst
new file mode 100644
index 0000000..991b3ac
--- /dev/null
+++ b/doc/index.rst
@@ -0,0 +1,35 @@
+Welcome to Masscanned documentation!
+====================================
+
+Introduction
+------------
+
+Masscanned is a low-interaction honeypot, primarily designed to help
+gather intelligence about network scanners and bots.
+
+It has been built as a companion tool for `IVRE
+`_ but can be used independently.
+
+The code is on `GitHub `_.
+
+Here is a quick demo:
+
+|demo|
+
+Status of this documentation
+----------------------------
+
+This documentation is a work in progress!
+
+Content
+-------
+
+.. toctree::
+ :maxdepth: 3
+ :caption: Usage:
+ :glob:
+
+ usage
+
+
+.. |demo| image:: img/demo.gif
diff --git a/doc/usage.rst b/doc/usage.rst
new file mode 100644
index 0000000..91e2c74
--- /dev/null
+++ b/doc/usage.rst
@@ -0,0 +1,92 @@
+Using Masscanned
+================
+
+Dedicated addresses
+~~~~~~~~~~~~~~~~~~~
+
+Masscanned is designed to handle its own IP addresses, which means
+that the host should not have those addresses configured, and
+Masscanned will answer ``ARP`` requests (or ``ICMPv6`` ``ND`` neighbor
+sollicitations).
+
+The host may have one or more (``IPv4`` and/or ``IPv6``) addresses configured
+on an interface also used by masscanned, but those addresses must be
+different from those configured to be used by masscanned.
+
+In that situation (dedicated addresses), just run:
+
+::
+
+ # masscanned -i -f
+
+where ```` is the path of a text file with one address (``IPv4``
+or ``IPv6``) per line.
+
+Addresses shared with the host
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Sometimes it is desirable to have an IP address used by the host
+(*e.g.*, for administration tasks) and by masscanned (to handle all
+other incoming packets).
+
+Since this is not implemented in masscanned, a tiny hack is needed: we
+are going to run it on a ``veth`` interface.
+
+For this example, we suppose:
+
+- The interface is ``eth0``, the address is ``192.168.0.10``.
+- We want masscanned to handle all the traffic except for incoming SSH
+ connections on TCP/22 port.
+
+We create a ``veth`` pair of interfaces, on which we are going to use
+the 0.255.0.0/31 network (which should not be a problem since
+0.0.0.0/8 is reserved as "Current Network"):
+
+::
+
+ # ip link add to_masscanned type veth peer masscanned
+ # ip link set masscanned up
+ # ip link set to_masscanned up
+ # ip addr add 0.255.0.0/31 dev to_masscanned
+ # masscanned -i masscanned
+
+Masscanned can now be used, but only from the host where it runs:
+
+::
+
+ # ping -c 1 0.255.0.1
+ PING 0.255.0.1 (0.255.0.1) 56(84) octets de données.
+ 64 octets de 0.255.0.1 : icmp_seq=1 ttl=64 temps=0.442 ms
+
+ --- statistiques ping 0.255.0.1 ---
+ 1 paquets transmis, 1 reçus, 0% packet loss, time 0ms
+ rtt min/avg/max/mdev = 0.442/0.442/0.442/0.000 ms
+
+Now, we are going to use Netfilter / ``iptables`` to redirect incoming
+traffic to masscanned:
+
+::
+
+ # sysctl -w net.ipv4.ip_forward=1
+ # iptables -t nat -A PREROUTING -i eth0 -d 192.168.0.10 -p tcp --dport 22 -j ACCEPT
+ # iptables -t nat -A PREROUTING -i eth0 -d 192.168.0.10/32 -j DNAT --to-destination 0.255.0.1
+
+And, from another host on the 192.168.0.0/24 network:
+
+::
+
+ # ping -c 1 192.168.0.10
+ PING 192.168.0.10 (192.168.0.10) 56(84) octets de données.
+ 64 octets de 192.168.0.10 : icmp_seq=1 ttl=63 temps=0.366 ms
+
+ --- statistiques ping 192.168.0.10 ---
+ 1 paquets transmis, 1 reçus, 0% packet loss, time 0ms
+ rtt min/avg/max/mdev = 0.366/0.366/0.366/0.000 ms
+
+
+The masscanned output:
+
+::
+
+ WARN - ARP-Reply to ea:c0:d6:20:0c:6a for IP 0.255.0.1
+ WARN - ICMP-Echo-Reply to ICMP-Echo-Request
diff --git a/docker/Dockerfile b/docker/Dockerfile
new file mode 100644
index 0000000..bbd46d9
--- /dev/null
+++ b/docker/Dockerfile
@@ -0,0 +1,45 @@
+# This file is part of masscanned.
+# Copyright 2021 - 2024 The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+FROM debian:12 AS fetcher
+
+RUN apt-get -q update && \
+ apt-get -qy --no-install-recommends install ca-certificates curl && \
+ curl -L https://github.com/ivre/masscanned/archive/refs/heads/master.tar.gz | tar zxf -
+
+
+FROM rust AS builder
+
+COPY --from=fetcher /masscanned-master /masscanned-master
+
+RUN cd masscanned-master && \
+ cargo build --release
+
+
+FROM debian:12
+LABEL maintainer="Pierre LALET "
+
+COPY --from=builder /masscanned-master/target/release/masscanned /usr/local/bin/masscanned
+
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-get -q update && \
+ apt-get -qy --no-install-recommends install iproute2 iptables && \
+ apt-get clean && rm -rf /var/lib/apt/lists/*
+
+COPY runmasscanned /usr/local/bin/runmasscanned
+
+CMD /usr/local/bin/runmasscanned
diff --git a/docker/Dockerfile-local b/docker/Dockerfile-local
new file mode 100644
index 0000000..e046e01
--- /dev/null
+++ b/docker/Dockerfile-local
@@ -0,0 +1,38 @@
+# This file is part of masscanned.
+# Copyright 2021 - 2024 The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+FROM rust AS builder
+
+ADD masscanned.tar ./
+
+RUN cd masscanned-master && \
+ cargo build --release
+
+
+FROM debian:12
+LABEL maintainer="Pierre LALET "
+
+COPY --from=builder /masscanned-master/target/release/masscanned /usr/local/bin/masscanned
+
+ENV DEBIAN_FRONTEND noninteractive
+
+RUN apt-get -q update && \
+ apt-get -qy --no-install-recommends install iproute2 iptables && \
+ apt-get clean && rm -rf /var/lib/apt/lists/*
+
+COPY runmasscanned /usr/local/bin/runmasscanned
+
+CMD /usr/local/bin/runmasscanned
diff --git a/docker/runmasscanned b/docker/runmasscanned
new file mode 100755
index 0000000..7953cb6
--- /dev/null
+++ b/docker/runmasscanned
@@ -0,0 +1,35 @@
+#! /bin/bash
+# This file is part of masscanned.
+# Copyright 2021 - 2023 The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+iface="$(ip route get 0.0.0.1 | awk '/^0\.0\.0\.1 via / {print $5}')"
+addrs="$(ip a show eth0 | awk '/ inet6? / {print $2}' | sed 's#/.*##' | tr '\n' ',' | sed 's#,$##')"
+
+if ! capsh --print | awk '/^Current: / {print $2}' | tr ',' '\n' | grep -q '^cap_net_admin$'; then
+ echo "WARNING: cannot run iptables (need capability cap_net_admin)" >&2
+ exit 1
+fi
+
+for v in '' 6; do
+ for c in INPUT OUTPUT FORWARD; do
+ ip${v}tables -P $c DROP
+ done
+done
+
+echo Interface: "$iface"
+echo Addresses: "$addrs"
+
+/usr/local/bin/masscanned -i "$iface" --self-ip-list "$addrs"
diff --git a/src/client/client_info.rs b/src/client/client_info.rs
index 3005a8e..a6d33a0 100644
--- a/src/client/client_info.rs
+++ b/src/client/client_info.rs
@@ -21,7 +21,7 @@ use std::net::IpAddr;
use pnet::packet::ip::IpNextHeaderProtocol;
use pnet::util::MacAddr;
-#[derive(PartialEq, Hash, Copy, Clone)]
+#[derive(PartialEq, Hash, Copy, Clone, Debug)]
pub struct ClientInfoSrcDst {
pub src: Option,
pub dst: Option,
@@ -35,7 +35,7 @@ pub struct ClientInfoSrcDst {
* - source and dest. transport port
* - syn cookie
**/
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, PartialEq, Debug)]
pub struct ClientInfo {
pub mac: ClientInfoSrcDst,
pub ip: ClientInfoSrcDst,
@@ -65,30 +65,6 @@ impl ClientInfo {
}
}
-impl PartialEq for ClientInfo {
- fn eq(&self, other: &Self) -> bool {
- if self.mac != other.mac {
- return false;
- }
- if self.ip != other.ip {
- return false;
- }
- if self.transport != other.transport {
- return false;
- }
- if self.port != other.port {
- return false;
- }
- /* this next case should never occur with TCP and UDP,
- * but this implementation tries to remain transport-protocol-agnostic
- **/
- if self.cookie != other.cookie {
- return false;
- }
- true
- }
-}
-
impl Eq for ClientInfo {}
impl Display for ClientInfo {
diff --git a/src/layer_2/arp.rs b/src/layer_2/arp.rs
index 4798c22..36ec6fe 100644
--- a/src/layer_2/arp.rs
+++ b/src/layer_2/arp.rs
@@ -29,20 +29,18 @@ pub fn repl<'a, 'b>(
arp_req: &'a ArpPacket,
masscanned: &Masscanned,
) -> Option> {
+ masscanned.log.arp_recv(arp_req);
let mut arp_repl =
MutableArpPacket::owned(arp_req.packet().to_vec()).expect("error parsing ARP packet");
/* Build ARP answer depending of the type of request */
match arp_req.get_operation() {
ArpOperations::Request => {
+ masscanned.log.arp_recv(arp_req);
let ip = IpAddr::V4(arp_req.get_target_proto_addr());
/* Ignore ARP requests for IP addresses not handled by masscanned */
- if let Some(ip_addr_list) = masscanned.ip_addresses {
+ if let Some(ip_addr_list) = masscanned.self_ip_list {
if !ip_addr_list.contains(&ip) {
- info!(
- "Ignoring ARP request from {} for IP {}",
- arp_req.get_sender_hw_addr(),
- ip
- );
+ masscanned.log.arp_drop(arp_req);
return None;
}
}
@@ -53,17 +51,15 @@ pub fn repl<'a, 'b>(
arp_repl.set_target_hw_addr(arp_req.get_sender_hw_addr().to_owned());
arp_repl.set_target_proto_addr(arp_req.get_sender_proto_addr().to_owned());
arp_repl.set_sender_proto_addr(arp_req.get_target_proto_addr().to_owned());
- warn!(
- "ARP-Reply to {} for IP {}",
- arp_req.get_sender_hw_addr(),
- arp_repl.get_sender_proto_addr()
- );
+ masscanned.log.arp_send(&arp_repl);
}
_ => {
info!("ARP Operation not handled: {:?}", arp_repl.get_operation());
+ masscanned.log.arp_drop(arp_req);
return None;
}
};
+ masscanned.log.arp_send(&arp_repl);
Some(arp_repl)
}
@@ -76,6 +72,8 @@ mod tests {
use pnet::util::MacAddr;
+ use crate::logger::MetaLogger;
+
#[test]
fn test_arp_reply() {
let mut ips = HashSet::new();
@@ -85,7 +83,9 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
let mut arp_req =
MutableArpPacket::owned([0; 28].to_vec()).expect("error constructing ARP request");
diff --git a/src/layer_2/mod.rs b/src/layer_2/mod.rs
index 75d4af8..82f2acb 100644
--- a/src/layer_2/mod.rs
+++ b/src/layer_2/mod.rs
@@ -104,30 +104,32 @@ pub fn reply<'a, 'b>(
masscanned: &Masscanned,
mut client_info: &mut ClientInfo,
) -> Option> {
- debug!("receiving Ethernet packet: {:?}", eth_req);
+ /* Fill client information for this packet with MAC addresses (src and dst) */
+ client_info.mac.src = Some(eth_req.get_source());
+ client_info.mac.dst = Some(eth_req.get_destination());
+ masscanned.log.eth_recv(eth_req, &client_info);
let mut eth_repl;
/* First, check if the destination MAC address is one of those masscanned
* is authorized to answer to (avoid answering to packets addressed to
* other machines)
**/
- if !get_authorized_eth_addr(&masscanned.mac, masscanned.ip_addresses)
+ if !get_authorized_eth_addr(&masscanned.mac, masscanned.self_ip_list)
.contains(ð_req.get_destination())
{
- info!(
- "Ignoring Ethernet packet from {} to {}",
- eth_req.get_source(),
- eth_req.get_destination(),
- );
+ masscanned.log.eth_drop(eth_req, &client_info);
return None;
}
- /* Fill client information for this packet with MAC addresses (src and dst) */
- client_info.mac.src = Some(eth_req.get_source());
- client_info.mac.dst = Some(eth_req.get_destination());
/* Build next layer payload for answer depending on the incoming packet */
match eth_req.get_ethertype() {
/* Construct answer to ARP request */
EtherTypes::Arp => {
- let arp_req = ArpPacket::new(eth_req.payload()).expect("error parsing ARP packet");
+ let arp_req = if let Some(p) = ArpPacket::new(eth_req.payload()) {
+ p
+ } else {
+ warn!("error parsing ARP packet");
+ masscanned.log.eth_drop(eth_req, &client_info);
+ return None;
+ };
if let Some(arp_repl) = arp::repl(&arp_req, masscanned) {
let arp_len = arp_repl.packet().len();
let eth_len = EthernetPacket::minimum_packet_size() + arp_len;
@@ -136,6 +138,7 @@ pub fn reply<'a, 'b>(
eth_repl.set_ethertype(EtherTypes::Arp);
eth_repl.set_payload(arp_repl.packet());
} else {
+ masscanned.log.eth_drop(eth_req, &client_info);
return None;
}
}
@@ -145,6 +148,7 @@ pub fn reply<'a, 'b>(
p
} else {
warn!("error parsing IPv4 packet");
+ masscanned.log.eth_drop(eth_req, &client_info);
return None;
};
if let Some(mut ipv4_repl) =
@@ -158,12 +162,19 @@ pub fn reply<'a, 'b>(
eth_repl.set_ethertype(EtherTypes::Ipv4);
eth_repl.set_payload(ipv4_repl.packet());
} else {
+ masscanned.log.eth_drop(eth_req, &client_info);
return None;
}
}
/* Construct answer to IPv6 packet */
EtherTypes::Ipv6 => {
- let ipv6_req = Ipv6Packet::new(eth_req.payload()).expect("error parsing IPv6 packet");
+ let ipv6_req = if let Some(p) = Ipv6Packet::new(eth_req.payload()) {
+ p
+ } else {
+ warn!("error parsing IPv6 packet");
+ masscanned.log.eth_drop(eth_req, &client_info);
+ return None;
+ };
if let Some(ipv6_repl) = layer_3::ipv6::repl(&ipv6_req, masscanned, &mut client_info) {
let ipv6_len = ipv6_repl.packet().len();
let eth_len = EthernetPacket::minimum_packet_size() + ipv6_len;
@@ -172,18 +183,20 @@ pub fn reply<'a, 'b>(
eth_repl.set_ethertype(EtherTypes::Ipv6);
eth_repl.set_payload(ipv6_repl.packet());
} else {
+ masscanned.log.eth_drop(eth_req, &client_info);
return None;
}
}
/* Log & drop unknown network protocol */
_ => {
info!("Ethernet type not handled: {:?}", eth_req.get_ethertype());
+ masscanned.log.eth_drop(eth_req, &client_info);
return None;
}
};
eth_repl.set_source(masscanned.mac);
eth_repl.set_destination(eth_req.get_source());
- debug!("sending Ethernet packet: {:?}", eth_repl);
+ masscanned.log.eth_send(ð_repl, &client_info);
Some(eth_repl)
}
@@ -193,6 +206,46 @@ mod tests {
use std::net::{Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
+ use crate::logger::MetaLogger;
+
+ #[test]
+ fn test_eth_empty() {
+ let payload = b"";
+ let test_mac_addr =
+ MacAddr::from_str("55:44:33:22:11:00").expect("error parsing MAC address");
+ let mac = MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address");
+ let mut client_info = ClientInfo::new();
+ let mut ips = HashSet::new();
+ ips.insert(IpAddr::V4(Ipv4Addr::new(0xaa, 0x99, 0x88, 0x77)));
+ ips.insert(IpAddr::V6(Ipv6Addr::new(
+ 0x7777, 0x7777, 0x7777, 0x7777, 0x7777, 0x7777, 0xaabb, 0xccdd,
+ )));
+ /* Construct masscanned context object */
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: mac,
+ iface: None,
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ for proto in [EtherTypes::Ipv4, EtherTypes::Ipv6, EtherTypes::Arp] {
+ let mut eth_req = MutableEthernetPacket::owned(vec![
+ 0;
+ EthernetPacket::minimum_packet_size(
+ ) + payload.len()
+ ])
+ .expect("error constructing ethernet packet");
+ eth_req.set_source(test_mac_addr);
+ eth_req.set_payload(payload);
+ eth_req.set_ethertype(proto);
+ eth_req.set_destination(mac);
+ if let Some(_) = reply(ð_req.to_immutable(), &masscanned, &mut client_info) {
+ panic!("expected no Ethernet answer, got one");
+ }
+ }
+ }
+
#[test]
fn test_eth_reply() {
/* test payload is IP(src="3.2.1.0", dst=".".join(str(b) for b in [0xaa, 0x99,
@@ -211,7 +264,9 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
let mut eth_req = MutableEthernetPacket::owned(vec![
0;
diff --git a/src/layer_3/ipv4.rs b/src/layer_3/ipv4.rs
index 6be97ba..5bff7a9 100644
--- a/src/layer_3/ipv4.rs
+++ b/src/layer_3/ipv4.rs
@@ -39,31 +39,43 @@ pub fn repl<'a, 'b>(
masscanned: &Masscanned,
mut client_info: &mut ClientInfo,
) -> Option> {
- debug!("receiving IPv4 packet: {:?}", ip_req);
+ /* Fill client info with source and dest. IP addresses */
+ client_info.ip.src = Some(IpAddr::V4(ip_req.get_source()));
+ client_info.ip.dst = Some(IpAddr::V4(ip_req.get_destination()));
+ masscanned.log.ipv4_recv(&ip_req, &client_info);
/* If masscanned is configured with IP addresses, then
* check that the dest. IP address of the packet is one of
* those handled by masscanned - otherwise, drop the packet.
**/
- if let Some(ip_addr_list) = masscanned.ip_addresses {
+ if let Some(ip_addr_list) = masscanned.self_ip_list {
if !ip_addr_list.contains(&IpAddr::V4(ip_req.get_destination())) {
- info!(
- "Ignoring IP packet from {} for {}",
- ip_req.get_source(),
- ip_req.get_destination()
- );
+ masscanned.log.ipv4_drop(&ip_req, &client_info);
+ return None;
+ }
+ }
+ /* If masscanned is configured with a remote ip deny list, then
+ * check if the src. IP address of the packet is one of
+ * those ignored by masscanned - if so, drop the packet.
+ **/
+ if let Some(remote_ip_deny_list) = masscanned.remote_ip_deny_list {
+ if remote_ip_deny_list.contains(&IpAddr::V4(ip_req.get_source())) {
+ masscanned.log.ipv4_drop(&ip_req, &client_info);
return None;
}
}
- /* Fill client info with source and dest. IP addresses */
- client_info.ip.src = Some(IpAddr::V4(ip_req.get_source()));
- client_info.ip.dst = Some(IpAddr::V4(ip_req.get_destination()));
/* Fill client info with transport layer procotol */
client_info.transport = Some(ip_req.get_next_level_protocol());
let mut ip_repl;
match ip_req.get_next_level_protocol() {
/* Answer to an ICMP packet */
IpNextHeaderProtocols::Icmp => {
- let icmp_req = IcmpPacket::new(ip_req.payload()).expect("error parsing ICMP packet");
+ let icmp_req = if let Some(p) = IcmpPacket::new(ip_req.payload()) {
+ p
+ } else {
+ warn!("error parsing ICMP packet");
+ masscanned.log.ipv4_drop(&ip_req, &client_info);
+ return None;
+ };
if let Some(mut icmp_repl) = layer_4::icmpv4::repl(&icmp_req, masscanned, &client_info)
{
icmp_repl.set_checksum(ipv4_checksum_icmp(&icmp_repl.to_immutable()));
@@ -77,12 +89,19 @@ pub fn repl<'a, 'b>(
ip_repl.set_payload(icmp_repl.packet());
ip_repl.set_next_level_protocol(IpNextHeaderProtocols::Icmp);
} else {
+ masscanned.log.ipv4_drop(&ip_req, &client_info);
return None;
}
}
/* Answer to a TCP packet */
IpNextHeaderProtocols::Tcp => {
- let tcp_req = TcpPacket::new(ip_req.payload()).expect("error parsing TCP packet");
+ let tcp_req = if let Some(p) = TcpPacket::new(ip_req.payload()) {
+ p
+ } else {
+ warn!("error parsing TCP packet");
+ masscanned.log.ipv4_drop(&ip_req, &client_info);
+ return None;
+ };
if let Some(mut tcp_repl) = layer_4::tcp::repl(&tcp_req, masscanned, &mut client_info) {
tcp_repl.set_checksum(ipv4_checksum_tcp(
&tcp_repl.to_immutable(),
@@ -99,12 +118,19 @@ pub fn repl<'a, 'b>(
ip_repl.set_payload(tcp_repl.packet());
ip_repl.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
} else {
+ masscanned.log.ipv4_drop(&ip_req, &client_info);
return None;
}
}
/* Answer to an UDP packet */
IpNextHeaderProtocols::Udp => {
- let udp_req = UdpPacket::new(ip_req.payload()).expect("error parsing UDP packet");
+ let udp_req = if let Some(p) = UdpPacket::new(ip_req.payload()) {
+ p
+ } else {
+ warn!("error parsing UDP packet");
+ masscanned.log.ipv4_drop(&ip_req, &client_info);
+ return None;
+ };
if let Some(mut udp_repl) = layer_4::udp::repl(&udp_req, masscanned, &mut client_info) {
udp_repl.set_checksum(ipv4_checksum_udp(
&udp_repl.to_immutable(),
@@ -123,15 +149,13 @@ pub fn repl<'a, 'b>(
ip_repl.set_payload(udp_repl.packet());
ip_repl.set_next_level_protocol(IpNextHeaderProtocols::Udp);
} else {
+ masscanned.log.ipv4_drop(&ip_req, &client_info);
return None;
}
}
/* Next layer protocol not handled (yet) - dropping packet */
_ => {
- info!(
- "IPv4 upper layer not handled: {:?}",
- ip_req.get_next_level_protocol()
- );
+ masscanned.log.ipv4_drop(&ip_req, &client_info);
return None;
}
};
@@ -150,7 +174,7 @@ pub fn repl<'a, 'b>(
/* FIXME when dest. was a multicast IP address */
ip_repl.set_source(ip_req.get_destination());
ip_repl.set_destination(ip_req.get_source());
- debug!("sending IPv4 packet: {:?}", ip_repl);
+ masscanned.log.ipv4_send(&ip_repl, &client_info);
Some(ip_repl)
}
@@ -163,10 +187,11 @@ mod tests {
use pnet::util::MacAddr;
+ use crate::logger::MetaLogger;
+
#[test]
- fn test_ipv4_reply() {
- /* test payload is scapy> ICMP() */
- let payload = b"\x08\x00\xf7\xff\x00\x00\x00\x00";
+ fn test_ipv4_empty() {
+ let payload = b"";
let mut client_info = ClientInfo::new();
let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0);
let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3);
@@ -177,7 +202,60 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ for proto in [
+ IpNextHeaderProtocols::Tcp,
+ IpNextHeaderProtocols::Udp,
+ IpNextHeaderProtocols::Icmp,
+ ] {
+ let mut ip_req = MutableIpv4Packet::owned(vec![
+ 0;
+ Ipv4Packet::minimum_packet_size()
+ + payload.len()
+ ])
+ .expect("error constructing IPv4 packet");
+ ip_req.set_version(4);
+ ip_req.set_ttl(64);
+ ip_req.set_identification(0);
+ ip_req.set_flags(Ipv4Flags::DontFragment);
+ ip_req.set_source(test_ip_addr);
+ ip_req.set_header_length(5);
+ /* Set test payload for layer 4 */
+ ip_req.set_total_length(ip_req.packet().len() as u16);
+ ip_req.set_payload(payload);
+ /* Set next protocol */
+ ip_req.set_next_level_protocol(proto);
+ /* Send to a legitimate IP address */
+ ip_req.set_destination(masscanned_ip_addr);
+ if let Some(_) = repl(&ip_req.to_immutable(), &masscanned, &mut client_info) {
+ panic!("expected no IP answer, got one");
+ }
+ }
+ }
+
+ #[test]
+ fn test_ipv4_reply() {
+ /* test payload is scapy> ICMP() */
+ let payload = b"\x08\x00\xf7\xff\x00\x00\x00\x00";
+ let mut client_info = ClientInfo::new();
+ let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0);
+ let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3);
+ let blacklist_ip_addr = Ipv4Addr::new(3, 3, 3, 3);
+ let mut ips = HashSet::new();
+ ips.insert(IpAddr::V4(masscanned_ip_addr));
+ let mut blacklist_ips = HashSet::new();
+ blacklist_ips.insert(IpAddr::V4(blacklist_ip_addr));
+ /* Construct masscanned context object */
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
+ iface: None,
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: Some(&blacklist_ips),
+ log: MetaLogger::new(),
};
let mut ip_req =
MutableIpv4Packet::owned(vec![0; Ipv4Packet::minimum_packet_size() + payload.len()])
@@ -206,5 +284,9 @@ mod tests {
/* Send to a non-legitimate IP address */
ip_req.set_destination(Ipv4Addr::new(2, 2, 2, 2));
assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None);
+ /* Send from a non-legitimate IP address */
+ ip_req.set_source(blacklist_ip_addr);
+ ip_req.set_destination(masscanned_ip_addr);
+ assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None);
}
}
diff --git a/src/layer_3/ipv6.rs b/src/layer_3/ipv6.rs
index edc5390..f482475 100644
--- a/src/layer_3/ipv6.rs
+++ b/src/layer_3/ipv6.rs
@@ -35,18 +35,31 @@ pub fn repl<'a, 'b>(
masscanned: &Masscanned,
mut client_info: &mut ClientInfo,
) -> Option> {
- debug!("receiving IPv6 packet: {:?}", ip_req);
+ /* Fill client info with source and dest. IP address */
+ client_info.ip.src = Some(IpAddr::V6(ip_req.get_source()));
+ client_info.ip.dst = Some(IpAddr::V6(ip_req.get_destination()));
+ masscanned.log.ipv6_recv(ip_req, client_info);
let src = ip_req.get_source();
let mut dst = ip_req.get_destination();
- /* If masscanned is configured with IP addresses, check that
- * the dest. IP address corresponds to one of those
- * Otherwise, drop the packet.
+ /* If masscanned is configured with IP addresses, then
+ * check that the dest. IP address of the packet is one of
+ * those handled by masscanned - otherwise, drop the packet.
**/
- if let Some(ip_addr_list) = masscanned.ip_addresses {
+ if let Some(ip_addr_list) = masscanned.self_ip_list {
if !ip_addr_list.contains(&IpAddr::V6(dst))
&& ip_req.get_next_header() != IpNextHeaderProtocols::Icmpv6
{
- info!("Ignoring IP packet from {} for {}", &src, &dst);
+ masscanned.log.ipv6_drop(ip_req, client_info);
+ return None;
+ }
+ }
+ /* If masscanned is configured with a remote ip deny list, then
+ * check if the src. IP address of the packet is one of
+ * those ignored by masscanned - if so, drop the packet.
+ **/
+ if let Some(remote_ip_deny_list) = masscanned.remote_ip_deny_list {
+ if remote_ip_deny_list.contains(&IpAddr::V6(src)) {
+ masscanned.log.ipv6_drop(ip_req, client_info);
return None;
}
}
@@ -59,8 +72,13 @@ pub fn repl<'a, 'b>(
match ip_req.get_next_header() {
/* Answer to ICMPv6 */
IpNextHeaderProtocols::Icmpv6 => {
- let icmp_req =
- Icmpv6Packet::new(ip_req.payload()).expect("error parsing ICMPv6 packet");
+ let icmp_req = if let Some(p) = Icmpv6Packet::new(ip_req.payload()) {
+ p
+ } else {
+ warn!("error parsing ICMPv6 packet");
+ masscanned.log.ipv6_drop(&ip_req, &client_info);
+ return None;
+ };
if let (Some(mut icmp_repl), dst_addr) =
layer_4::icmpv6::repl(&icmp_req, masscanned, &client_info)
{
@@ -84,12 +102,19 @@ pub fn repl<'a, 'b>(
ip_repl.set_hop_limit(255);
};
} else {
+ masscanned.log.ipv6_drop(ip_req, client_info);
return None;
}
}
/* Answer to TCP */
IpNextHeaderProtocols::Tcp => {
- let tcp_req = TcpPacket::new(ip_req.payload()).expect("error parsing TCP packet");
+ let tcp_req = if let Some(p) = TcpPacket::new(ip_req.payload()) {
+ p
+ } else {
+ warn!("error parsing TCP packet");
+ masscanned.log.ipv6_drop(&ip_req, &client_info);
+ return None;
+ };
if let Some(mut tcp_repl) = layer_4::tcp::repl(&tcp_req, masscanned, &mut client_info) {
/* Compute and set TCP checksum */
tcp_repl.set_checksum(ipv6_checksum_tcp(
@@ -108,12 +133,19 @@ pub fn repl<'a, 'b>(
ip_repl.set_payload_length(tcp_len as u16);
ip_repl.set_payload(&tcp_repl.packet());
} else {
+ masscanned.log.ipv6_drop(ip_req, client_info);
return None;
}
}
/* Answer to UDP */
IpNextHeaderProtocols::Udp => {
- let udp_req = UdpPacket::new(ip_req.payload()).expect("error parsing UDP packet");
+ let udp_req = if let Some(p) = UdpPacket::new(ip_req.payload()) {
+ p
+ } else {
+ warn!("error parsing UDP packet");
+ masscanned.log.ipv6_drop(&ip_req, &client_info);
+ return None;
+ };
if let Some(mut udp_repl) = layer_4::udp::repl(&udp_req, masscanned, &mut client_info) {
/* Compute and set UDP checksum */
udp_repl.set_checksum(ipv6_checksum_udp(
@@ -132,15 +164,13 @@ pub fn repl<'a, 'b>(
ip_repl.set_payload_length(udp_len as u16);
ip_repl.set_payload(&udp_repl.packet());
} else {
+ masscanned.log.ipv6_drop(ip_req, client_info);
return None;
}
}
/* Other protocols are not handled (yet) - dropping */
_ => {
- info!(
- "IPv6 upper layer not handled: {:?}",
- ip_req.get_next_header()
- );
+ masscanned.log.ipv6_drop(ip_req, client_info);
return None;
}
};
@@ -153,7 +183,7 @@ pub fn repl<'a, 'b>(
/* Set packet source and dest. */
ip_repl.set_source(dst);
ip_repl.set_destination(src);
- debug!("sending IPv6 packet: {:?}", ip_repl);
+ masscanned.log.ipv6_send(&ip_repl, client_info);
Some(ip_repl)
}
@@ -166,12 +196,11 @@ mod tests {
use pnet::util::MacAddr;
+ use crate::logger::MetaLogger;
+
#[test]
- fn test_ipv6_reply() {
- /* test payload is scapy> IPv6(src="7777:6666:5555:4444:3333:2222:1111:0000",
- * dst="0000:1111:2222:3333:4444:5555:6666:7777")/TCP(sport=12345, dport=54321,
- * flags="S"))[TCP] */
- let payload = b"09\xd41\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xcf\xbc\x00\x00";
+ fn test_ipv6_empty() {
+ let payload = b"";
let mut client_info = ClientInfo::new();
let test_ip_addr = Ipv6Addr::new(
0x7777, 0x6666, 0x5555, 0x4444, 0x3333, 0x2222, 0x1111, 0x0000,
@@ -186,7 +215,64 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ for proto in [
+ IpNextHeaderProtocols::Tcp,
+ IpNextHeaderProtocols::Udp,
+ IpNextHeaderProtocols::Icmp,
+ ] {
+ let mut ip_req = MutableIpv6Packet::owned(vec![
+ 0;
+ Ipv6Packet::minimum_packet_size()
+ + payload.len()
+ ])
+ .expect("error constructing IPv6 packet");
+ ip_req.set_version(6);
+ ip_req.set_source(test_ip_addr);
+ /* Set test payload for layer 4 */
+ ip_req.set_payload_length(payload.len() as u16);
+ ip_req.set_payload(payload);
+ /* Set next protocol */
+ ip_req.set_next_header(proto);
+ /* Send to a legitimate IP address */
+ ip_req.set_destination(masscanned_ip_addr);
+ if let Some(_) = repl(&ip_req.to_immutable(), &masscanned, &mut client_info) {
+ panic!("expected no IP answer, got one");
+ }
+ }
+ }
+
+ #[test]
+ fn test_ipv6_reply() {
+ /* test payload is scapy> IPv6(src="7777:6666:5555:4444:3333:2222:1111:0000",
+ * dst="0000:1111:2222:3333:4444:5555:6666:7777")/TCP(sport=12345, dport=54321,
+ * flags="S"))[TCP] */
+ let payload = b"09\xd41\x00\x00\x00\x00\x00\x00\x00\x00P\x02 \x00\xcf\xbc\x00\x00";
+ let mut client_info = ClientInfo::new();
+ let test_ip_addr = Ipv6Addr::new(
+ 0x7777, 0x6666, 0x5555, 0x4444, 0x3333, 0x2222, 0x1111, 0x0000,
+ );
+ let masscanned_ip_addr = Ipv6Addr::new(
+ 0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7777,
+ );
+ let blacklist_ip_addr = Ipv6Addr::new(
+ 0x1111, 0x1111, 0x1111, 0x1111, 0x1111, 0x1111, 0x1111, 0x1111,
+ );
+ let mut ips = HashSet::new();
+ ips.insert(IpAddr::V6(masscanned_ip_addr));
+ let mut blacklist_ips = HashSet::new();
+ blacklist_ips.insert(IpAddr::V6(blacklist_ip_addr));
+ /* Construct masscanned context object */
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
+ iface: None,
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: Some(&blacklist_ips),
+ log: MetaLogger::new(),
};
let mut ip_req =
MutableIpv6Packet::owned(vec![0; Ipv6Packet::minimum_packet_size() + payload.len()])
@@ -213,5 +299,9 @@ mod tests {
0x0000, 0x1111, 0x2222, 0x3333, 0x4444, 0x5555, 0x6666, 0x7778,
));
assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None);
+ /* Send from a non-legitimate IP address */
+ ip_req.set_source(blacklist_ip_addr);
+ ip_req.set_destination(masscanned_ip_addr);
+ assert!(repl(&ip_req.to_immutable(), &masscanned, &mut client_info) == None);
}
}
diff --git a/src/layer_4/icmpv4.rs b/src/layer_4/icmpv4.rs
index 95cdf7c..becef8d 100644
--- a/src/layer_4/icmpv4.rs
+++ b/src/layer_4/icmpv4.rs
@@ -14,8 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Masscanned. If not, see .
-use log::*;
-
use pnet::packet::{
icmp::{IcmpCode, IcmpPacket, IcmpTypes, MutableIcmpPacket},
Packet,
@@ -26,16 +24,16 @@ use crate::Masscanned;
pub fn repl<'a, 'b>(
icmp_req: &'a IcmpPacket,
- _masscanned: &Masscanned,
- mut _client_info: &ClientInfo,
+ masscanned: &Masscanned,
+ client_info: &ClientInfo,
) -> Option> {
- debug!("receiving ICMPv4 packet: {:?}", icmp_req);
+ masscanned.log.icmpv4_recv(icmp_req, client_info);
let mut icmp_repl;
match icmp_req.get_icmp_type() {
IcmpTypes::EchoRequest => {
/* Check code of ICMP packet */
if icmp_req.get_icmp_code() != IcmpCode(0) {
- info!("ICMP code not handled: {:?}", icmp_req.get_icmp_code());
+ masscanned.log.icmpv4_drop(icmp_req, client_info);
return None;
}
/* Compute answer length */
@@ -53,13 +51,13 @@ pub fn repl<'a, 'b>(
* reply message."
**/
icmp_repl.set_payload(icmp_req.payload());
- warn!("ICMP-Echo-Reply to ICMP-Echo-Request");
}
_ => {
+ masscanned.log.icmpv4_drop(icmp_req, client_info);
return None;
}
};
- debug!("sending ICMPv4 packet: {:?}", icmp_repl);
+ masscanned.log.icmpv4_send(&icmp_repl, client_info);
Some(icmp_repl)
}
@@ -70,6 +68,8 @@ mod tests {
use pnet::util::MacAddr;
+ use crate::logger::MetaLogger;
+
#[test]
fn test_icmpv4_reply() {
/* test payload is scapy> ICMP() */
@@ -80,7 +80,9 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
let mut icmp_req =
MutableIcmpPacket::owned(vec![0; IcmpPacket::minimum_packet_size() + payload.len()])
diff --git a/src/layer_4/icmpv6.rs b/src/layer_4/icmpv6.rs
index 6db03d2..00c53f4 100644
--- a/src/layer_4/icmpv6.rs
+++ b/src/layer_4/icmpv6.rs
@@ -40,7 +40,7 @@ pub fn nd_ns_repl<'a, 'b>(
* check that the dest. IP address of the packet is one of
* those handled by masscanned - otherwise, drop the packet.
**/
- if let Some(addresses) = masscanned.ip_addresses {
+ if let Some(addresses) = masscanned.self_ip_list {
if !addresses.contains(&IpAddr::V6(nd_ns_req.get_target_addr())) {
return None;
}
@@ -103,7 +103,7 @@ pub fn repl<'a, 'b>(
masscanned: &Masscanned,
client_info: &ClientInfo,
) -> (Option>, Option) {
- debug!("receiving ICMPv6 packet: {:?}", icmp_req);
+ masscanned.log.icmpv6_recv(icmp_req, client_info);
let mut dst_ip = None;
if icmp_req.get_icmpv6_code() != Icmpv6Codes::NoCode {
return (None, None);
@@ -120,6 +120,7 @@ pub fn repl<'a, 'b>(
icmp_repl = MutableIcmpv6Packet::owned(nd_na_repl.packet().to_vec())
.expect("error constructing an ICMPv6 packet");
} else {
+ masscanned.log.icmpv6_drop(icmp_req, client_info);
return (None, None);
}
}
@@ -136,17 +137,13 @@ pub fn repl<'a, 'b>(
icmp_repl = MutableIcmpv6Packet::owned(vec![0; Icmpv6Packet::packet_size(&echo_repl)])
.expect("error constructing an ICMPv6 packet");
icmp_repl.populate(&echo_repl);
- warn!("ICMPv6-Echo-Reply to ICMPv6-Echo-Request");
}
_ => {
- info!(
- "ICMPv6 packet not handled: {:?}",
- icmp_req.get_icmpv6_type()
- );
+ masscanned.log.icmpv6_drop(icmp_req, client_info);
return (None, None);
}
};
- debug!("sending ICMPv6 packet: {:?}", icmp_repl);
+ masscanned.log.icmpv6_send(&icmp_repl, client_info);
(Some(icmp_repl), dst_ip)
}
@@ -160,6 +157,8 @@ mod tests {
use pnet::packet::icmpv6::ndp::{MutableNeighborSolicitPacket, NeighborSolicit};
use pnet::util::MacAddr;
+ use crate::logger::MetaLogger;
+
#[test]
fn test_nd_na_reply() {
let client_info = ClientInfo::new();
@@ -173,7 +172,9 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
/* Legitimate solicitation */
let ndp_ns = NeighborSolicit {
@@ -245,7 +246,9 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
let mut icmpv6_echo_req = MutableIcmpv6Packet::owned(vec![
0;
diff --git a/src/layer_4/tcp.rs b/src/layer_4/tcp.rs
index d93585f..82ad0c2 100644
--- a/src/layer_4/tcp.rs
+++ b/src/layer_4/tcp.rs
@@ -31,10 +31,10 @@ pub fn repl<'a, 'b>(
masscanned: &Masscanned,
mut client_info: &mut ClientInfo,
) -> Option> {
- debug!("receiving TCP packet: {:?}", tcp_req);
/* Fill client info with source and dest. TCP port */
client_info.port.src = Some(tcp_req.get_source());
client_info.port.dst = Some(tcp_req.get_destination());
+ masscanned.log.tcp_recv(tcp_req, client_info);
/* Construct response TCP packet */
let mut tcp_repl;
match tcp_req.get_flags() {
@@ -49,16 +49,24 @@ pub fn repl<'a, 'b>(
};
/* Compute syncookie */
if let Ok(cookie) = synackcookie::generate(&client_info, &masscanned.synack_key) {
- if cookie != ackno {
- info!("PSH-ACK ignored: synackcookie not valid");
- return None;
- }
client_info.cookie = Some(cookie);
+ if !proto::is_tcb_set(cookie) {
+ /* First Ack: check syncookie, create tcb */
+ if cookie != ackno {
+ masscanned.log.tcp_drop(tcp_req, client_info);
+ return None;
+ }
+ proto::add_tcb(cookie);
+ }
}
warn!("ACK to PSH-ACK on port {}", tcp_req.get_destination());
let payload = tcp_req.payload();
/* Any answer to upper-layer protocol? */
- if let Some(repl) = proto::repl(&payload, masscanned, &mut client_info) {
+ let mut payload_repl = None;
+ proto::get_tcb(client_info.cookie.unwrap(), |tcb| {
+ payload_repl = proto::repl(&payload, masscanned, &mut client_info, tcb);
+ });
+ if let Some(repl) = payload_repl {
tcp_repl = MutableTcpPacket::owned(
[vec![0; MutableTcpPacket::minimum_packet_size()], repl].concat(),
)
@@ -70,33 +78,52 @@ pub fn repl<'a, 'b>(
.expect("error constructing a TCP packet");
tcp_repl.set_flags(TcpFlags::ACK);
}
- tcp_repl.set_acknowledgement(tcp_req.get_sequence() + (tcp_req.payload().len() as u32));
+ tcp_repl.set_acknowledgement(
+ tcp_req
+ .get_sequence()
+ .wrapping_add(tcp_req.payload().len() as u32),
+ );
tcp_repl.set_sequence(tcp_req.get_acknowledgement());
}
/* Answer to ACK: nothing */
flags if flags == TcpFlags::ACK => {
/* answer here when server needs to speak first after handshake */
+ masscanned.log.tcp_drop(tcp_req, client_info);
return None;
}
- /* Answer to RST and FIN: nothing */
- flags if (flags == TcpFlags::RST || flags == (TcpFlags::FIN | TcpFlags::ACK)) => {
+ /* Answer to RST: nothing */
+ flags if flags == TcpFlags::RST => {
+ masscanned.log.tcp_drop(tcp_req, client_info);
return None;
}
- /* Answer to SYN */
- flags if flags & TcpFlags::SYN == TcpFlags::SYN => {
+ /* Answer to FIN,ACK with FIN,ACK */
+ flags if flags == (TcpFlags::FIN | TcpFlags::ACK) => {
+ tcp_repl = MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()])
+ .expect("error constructing a TCP packet");
+ tcp_repl.set_flags(TcpFlags::FIN | TcpFlags::ACK);
+ tcp_repl.set_acknowledgement(tcp_req.get_sequence().wrapping_add(1));
+ tcp_repl.set_sequence(tcp_req.get_acknowledgement());
+ }
+ /* Answer to SYN + P|U|C|E + !(C && E) to imitate Linux network stack */
+ flags
+ if (flags & TcpFlags::SYN) == TcpFlags::SYN &&
+ /* no other flag than S,P,U,C,E */
+ (flags & !(TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG | TcpFlags::CWR | TcpFlags::ECE)) == 0 &&
+ /* not C && E */
+ ((flags & TcpFlags::CWR == 0) || (flags & TcpFlags::ECE == 0)) =>
+ {
tcp_repl = MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()])
.expect("error constructing a TCP packet");
tcp_repl.set_flags(TcpFlags::ACK);
tcp_repl.set_flags(TcpFlags::SYN | TcpFlags::ACK);
- tcp_repl.set_acknowledgement(tcp_req.get_sequence() + 1);
+ tcp_repl.set_acknowledgement(tcp_req.get_sequence().wrapping_add(1));
/* generate a SYNACK-cookie (same as masscan) */
tcp_repl.set_sequence(
synackcookie::generate(&client_info, &masscanned.synack_key).unwrap(),
);
- warn!("SYN-ACK to ACK on port {}", tcp_req.get_destination());
}
_ => {
- info!("TCP flag not handled: {}", tcp_req.get_flags());
+ masscanned.log.tcp_drop(tcp_req, client_info);
return None;
}
}
@@ -107,7 +134,7 @@ pub fn repl<'a, 'b>(
/* Set TCP headers */
tcp_repl.set_data_offset(5);
tcp_repl.set_window(65535);
- debug!("sending TCP packet: {:?}", tcp_repl);
+ masscanned.log.tcp_send(&tcp_repl, client_info);
Some(tcp_repl)
}
@@ -118,13 +145,212 @@ mod tests {
use pnet::util::MacAddr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
+ use crate::logger::MetaLogger;
+
+ #[test]
+ fn test_tcp_syn() {
+ let masscanned = Masscanned {
+ mac: MacAddr(0, 0, 0, 0, 0, 0),
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f],
+ iface: None,
+ log: MetaLogger::new(),
+ };
+ /* reference */
+ let ip_src = IpAddr::V4(Ipv4Addr::new(27, 198, 143, 1));
+ let ip_dst = IpAddr::V4(Ipv4Addr::new(90, 64, 122, 203));
+ let tcp_sport = 65500;
+ let tcp_dport = 80;
+ let seq = 1234567;
+ let ack = 0;
+ let mut client_info = ClientInfo {
+ mac: ClientInfoSrcDst {
+ src: None,
+ dst: None,
+ },
+ ip: ClientInfoSrcDst {
+ src: Some(ip_src),
+ dst: Some(ip_dst),
+ },
+ transport: None,
+ port: ClientInfoSrcDst {
+ src: Some(tcp_sport),
+ dst: Some(tcp_dport),
+ },
+ cookie: None,
+ };
+ /* flags OK - list is exhaustive */
+ /* aim at imitating a Linux network stack */
+ let flags_ok = [
+ TcpFlags::SYN,
+ TcpFlags::SYN | TcpFlags::PSH,
+ TcpFlags::SYN | TcpFlags::URG,
+ TcpFlags::SYN | TcpFlags::CWR,
+ TcpFlags::SYN | TcpFlags::ECE,
+ TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG,
+ TcpFlags::SYN | TcpFlags::PSH | TcpFlags::CWR,
+ TcpFlags::SYN | TcpFlags::PSH | TcpFlags::ECE,
+ TcpFlags::SYN | TcpFlags::PSH | TcpFlags::ECE,
+ TcpFlags::SYN | TcpFlags::URG | TcpFlags::CWR,
+ TcpFlags::SYN | TcpFlags::URG | TcpFlags::ECE,
+ TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG | TcpFlags::CWR,
+ TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG | TcpFlags::ECE,
+ ];
+ for flags in flags_ok {
+ let mut tcp_req =
+ MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap();
+ tcp_req.set_source(tcp_sport);
+ tcp_req.set_destination(tcp_dport);
+ tcp_req.set_sequence(seq);
+ tcp_req.set_acknowledgement(ack);
+ tcp_req.set_flags(flags);
+ let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info);
+ if some_tcp_repl == None {
+ panic!("expected a reply, got none for flags: {:?}", flags);
+ }
+ let tcp_repl = some_tcp_repl.unwrap();
+ /* check reply flags */
+ assert!(tcp_repl.get_flags() == (TcpFlags::SYN | TcpFlags::ACK));
+ /* check reply seq and ack */
+ assert!(tcp_repl.get_acknowledgement() == seq.wrapping_add(1));
+ }
+ /* flags KO - list is *not* exhaustive */
+ let flags_ko = [
+ TcpFlags::SYN | TcpFlags::ACK,
+ TcpFlags::SYN | TcpFlags::FIN,
+ TcpFlags::SYN | TcpFlags::CWR | TcpFlags::ECE,
+ TcpFlags::SYN | TcpFlags::PSH | TcpFlags::URG | TcpFlags::CWR | TcpFlags::ECE,
+ TcpFlags::PSH,
+ ];
+ for flags in flags_ko {
+ let mut tcp_req =
+ MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap();
+ tcp_req.set_source(tcp_sport);
+ tcp_req.set_destination(tcp_dport);
+ tcp_req.set_sequence(seq);
+ tcp_req.set_acknowledgement(ack);
+ tcp_req.set_flags(flags);
+ let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info);
+ if some_tcp_repl != None {
+ panic!("expected no reply, got one");
+ }
+ }
+ }
+
+ #[test]
+ fn test_tcp_fin_ack() {
+ let masscanned = Masscanned {
+ mac: MacAddr(0, 0, 0, 0, 0, 0),
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f],
+ iface: None,
+ log: MetaLogger::new(),
+ };
+ /* reference */
+ let ip_src = IpAddr::V4(Ipv4Addr::new(27, 198, 143, 1));
+ let ip_dst = IpAddr::V4(Ipv4Addr::new(90, 64, 122, 203));
+ let tcp_sport = 65500;
+ let tcp_dport = 80;
+ let seq = 1234567;
+ let ack = 7654321;
+ let mut client_info = ClientInfo {
+ mac: ClientInfoSrcDst {
+ src: None,
+ dst: None,
+ },
+ ip: ClientInfoSrcDst {
+ src: Some(ip_src),
+ dst: Some(ip_dst),
+ },
+ transport: None,
+ port: ClientInfoSrcDst {
+ src: Some(tcp_sport),
+ dst: Some(tcp_dport),
+ },
+ cookie: None,
+ };
+ let mut tcp_req =
+ MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap();
+ tcp_req.set_source(tcp_sport);
+ tcp_req.set_destination(tcp_dport);
+ tcp_req.set_sequence(seq);
+ tcp_req.set_acknowledgement(ack);
+ tcp_req.set_flags(TcpFlags::FIN | TcpFlags::ACK);
+ let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info);
+ if some_tcp_repl == None {
+ panic!("expected a reply, got none");
+ }
+ let tcp_repl = some_tcp_repl.unwrap();
+ /* check reply flags */
+ assert!(tcp_repl.get_flags() == (TcpFlags::FIN | TcpFlags::ACK));
+ /* check reply seq and ack */
+ assert!(tcp_repl.get_sequence() == ack);
+ assert!(tcp_repl.get_acknowledgement() == seq.wrapping_add(1));
+ }
+
+ #[test]
+ fn test_tcp_fin_ack_wrap() {
+ let masscanned = Masscanned {
+ mac: MacAddr(0, 0, 0, 0, 0, 0),
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f],
+ iface: None,
+ log: MetaLogger::new(),
+ };
+ /* reference */
+ let ip_src = IpAddr::V4(Ipv4Addr::new(27, 198, 143, 1));
+ let ip_dst = IpAddr::V4(Ipv4Addr::new(90, 64, 122, 203));
+ let tcp_sport = 65500;
+ let tcp_dport = 80;
+ let seq = 0xffffffff;
+ let ack = 0xffffffff;
+ let mut client_info = ClientInfo {
+ mac: ClientInfoSrcDst {
+ src: None,
+ dst: None,
+ },
+ ip: ClientInfoSrcDst {
+ src: Some(ip_src),
+ dst: Some(ip_dst),
+ },
+ transport: None,
+ port: ClientInfoSrcDst {
+ src: Some(tcp_sport),
+ dst: Some(tcp_dport),
+ },
+ cookie: None,
+ };
+ let mut tcp_req =
+ MutableTcpPacket::owned(vec![0; MutableTcpPacket::minimum_packet_size()]).unwrap();
+ tcp_req.set_source(tcp_sport);
+ tcp_req.set_destination(tcp_dport);
+ tcp_req.set_sequence(seq);
+ tcp_req.set_acknowledgement(ack);
+ tcp_req.set_flags(TcpFlags::FIN | TcpFlags::ACK);
+ let some_tcp_repl = repl(&tcp_req.to_immutable(), &masscanned, &mut client_info);
+ if some_tcp_repl == None {
+ panic!("expected a reply, got none");
+ }
+ let tcp_repl = some_tcp_repl.unwrap();
+ /* check reply flags */
+ assert!(tcp_repl.get_flags() == (TcpFlags::FIN | TcpFlags::ACK));
+ /* check reply seq and ack */
+ assert!(tcp_repl.get_sequence() == ack);
+ assert!(tcp_repl.get_acknowledgement() == seq.wrapping_add(1));
+ }
+
#[test]
fn test_synack_cookie_ipv4() {
let masscanned = Masscanned {
mac: MacAddr(0, 0, 0, 0, 0, 0),
- ip_addresses: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f],
iface: None,
+ log: MetaLogger::new(),
};
/* reference */
let ip_src = IpAddr::V4(Ipv4Addr::new(27, 198, 143, 1));
@@ -171,9 +397,11 @@ mod tests {
fn test_synack_cookie_ipv6() {
let masscanned = Masscanned {
mac: MacAddr(0, 0, 0, 0, 0, 0),
- ip_addresses: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
synack_key: [0x06a0a1d63f305e9b, 0xd4d4bcbb7304875f],
iface: None,
+ log: MetaLogger::new(),
};
/* reference */
let ip_src = IpAddr::V6(Ipv6Addr::new(234, 52, 183, 47, 184, 172, 64, 141));
diff --git a/src/layer_4/udp.rs b/src/layer_4/udp.rs
index cdc1d47..57d147f 100644
--- a/src/layer_4/udp.rs
+++ b/src/layer_4/udp.rs
@@ -14,8 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Masscanned. If not, see .
-use log::*;
-
use pnet::packet::{
udp::{MutableUdpPacket, UdpPacket},
Packet,
@@ -30,25 +28,26 @@ pub fn repl<'a, 'b>(
masscanned: &Masscanned,
mut client_info: &mut ClientInfo,
) -> Option> {
- debug!("receiving UDP packet: {:?}", udp_req);
/* Fill client info with source and dest. UDP port */
client_info.port.src = Some(udp_req.get_source());
client_info.port.dst = Some(udp_req.get_destination());
+ masscanned.log.udp_recv(udp_req, client_info);
let payload = udp_req.payload();
let mut udp_repl;
- if let Some(repl) = proto::repl(&payload, masscanned, &mut client_info) {
+ if let Some(repl) = proto::repl(&payload, masscanned, &mut client_info, None) {
udp_repl = MutableUdpPacket::owned(
[vec![0; MutableUdpPacket::minimum_packet_size()], repl].concat(),
)
.expect("error constructing a UDP packet");
udp_repl.set_length(udp_repl.packet().len() as u16);
} else {
+ masscanned.log.udp_drop(udp_req, client_info);
return None;
}
/* Set source and dest. port for response packet from client info */
/* Note: client info could have been modified by upper layers (e.g., STUN) */
udp_repl.set_source(client_info.port.dst.unwrap());
udp_repl.set_destination(client_info.port.src.unwrap());
- debug!("sending UDP packet: {:?}", udp_repl);
+ masscanned.log.udp_send(&udp_repl, client_info);
Some(udp_repl)
}
diff --git a/src/logger/console.rs b/src/logger/console.rs
new file mode 100644
index 0000000..fe7f7ec
--- /dev/null
+++ b/src/logger/console.rs
@@ -0,0 +1,308 @@
+// This file is part of masscanned.
+// Copyright 2021 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use std::time::SystemTime;
+
+use pnet::packet::{
+ arp::{ArpPacket, MutableArpPacket},
+ ethernet::{EthernetPacket, MutableEthernetPacket},
+ icmp::{IcmpPacket, MutableIcmpPacket},
+ icmpv6::{Icmpv6Packet, MutableIcmpv6Packet},
+ ipv4::{Ipv4Packet, MutableIpv4Packet},
+ ipv6::{Ipv6Packet, MutableIpv6Packet},
+ tcp::{MutableTcpPacket, TcpPacket},
+ udp::{MutableUdpPacket, UdpPacket},
+};
+
+use crate::client::ClientInfo;
+use crate::logger::Logger;
+
+pub struct ConsoleLogger {
+ arp: bool,
+ eth: bool,
+ ipv4: bool,
+ ipv6: bool,
+ icmpv4: bool,
+ icmpv6: bool,
+ tcp: bool,
+ udp: bool,
+}
+
+impl ConsoleLogger {
+ pub fn new() -> Self {
+ ConsoleLogger {
+ arp: true,
+ eth: true,
+ ipv4: true,
+ ipv6: true,
+ icmpv4: true,
+ icmpv6: true,
+ tcp: true,
+ udp: true,
+ }
+ }
+ fn prolog(&self, proto: &str, verb: &str, crlf: bool) {
+ let now = SystemTime::now()
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .unwrap();
+ print!(
+ "{}.{}\t{}\t{}{}",
+ now.as_secs(),
+ now.subsec_millis(),
+ proto,
+ verb,
+ if crlf { "\n" } else { "\t" },
+ );
+ }
+ fn client_info(&self, c: &ClientInfo) {
+ print!(
+ "{}\t{}\t{}\t{}\t{}\t{}\t{}\t",
+ if let Some(m) = c.mac.src {
+ format!("{}", m)
+ } else {
+ "".to_string()
+ },
+ if let Some(m) = c.mac.dst {
+ format!("{}", m)
+ } else {
+ "".to_string()
+ },
+ if let Some(i) = c.ip.src {
+ format!("{}", i)
+ } else {
+ "".to_string()
+ },
+ if let Some(i) = c.ip.dst {
+ format!("{}", i)
+ } else {
+ "".to_string()
+ },
+ if let Some(t) = c.transport {
+ format!("{}", t)
+ } else {
+ "".to_string()
+ },
+ if let Some(p) = c.port.src {
+ format!("{}", p)
+ } else {
+ "".to_string()
+ },
+ if let Some(p) = c.port.dst {
+ format!("{}", p)
+ } else {
+ "".to_string()
+ },
+ );
+ }
+}
+
+impl Logger for ConsoleLogger {
+ fn init(&self) {
+ self.prolog("arp", "init", true);
+ self.prolog("eth", "init", true);
+ self.prolog("ipv4", "init", true);
+ self.prolog("ipv6", "init", true);
+ self.prolog("icmpv4", "init", true);
+ self.prolog("icmpv6", "init", true);
+ self.prolog("tcp", "init", true);
+ self.prolog("udp", "init", true);
+ }
+ /* ARP */
+ fn arp_enabled(&self) -> bool {
+ self.arp
+ }
+ fn arp_recv(&self, p: &ArpPacket) {
+ self.prolog("arp", "recv", false);
+ println!(
+ "{:}\t{:}\t{:}\t{:}\t{:?}",
+ p.get_sender_hw_addr(),
+ p.get_target_hw_addr(),
+ p.get_sender_proto_addr(),
+ p.get_target_proto_addr(),
+ p.get_operation(),
+ );
+ }
+ fn arp_drop(&self, p: &ArpPacket) {
+ self.prolog("arp", "drop", false);
+ println!(
+ "{:}\t{:}\t{:}\t{:}\t{:?}",
+ p.get_sender_hw_addr(),
+ p.get_target_hw_addr(),
+ p.get_sender_proto_addr(),
+ p.get_target_proto_addr(),
+ p.get_operation(),
+ );
+ }
+ fn arp_send(&self, p: &MutableArpPacket) {
+ self.prolog("arp", "send", false);
+ println!(
+ "{:}\t{:}\t{:}\t{:}\t{:?}",
+ p.get_target_hw_addr(),
+ p.get_sender_hw_addr(),
+ p.get_target_proto_addr(),
+ p.get_sender_proto_addr(),
+ p.get_operation(),
+ );
+ }
+ /* Ethernet */
+ fn eth_enabled(&self) -> bool {
+ self.eth
+ }
+ fn eth_recv(&self, p: &EthernetPacket, c: &ClientInfo) {
+ self.prolog("eth", "recv", false);
+ self.client_info(c);
+ println!("{:}", p.get_ethertype(),);
+ }
+ fn eth_drop(&self, p: &EthernetPacket, c: &ClientInfo) {
+ self.prolog("eth", "drop", false);
+ self.client_info(c);
+ println!("{:}", p.get_ethertype(),);
+ }
+ fn eth_send(&self, p: &MutableEthernetPacket, c: &ClientInfo) {
+ self.prolog("eth", "send", false);
+ self.client_info(c);
+ println!("{:}", p.get_ethertype(),);
+ }
+ /* IPv4 */
+ fn ipv4_enabled(&self) -> bool {
+ self.ipv4
+ }
+ fn ipv4_recv(&self, p: &Ipv4Packet, c: &ClientInfo) {
+ self.prolog("ipv4", "recv", false);
+ self.client_info(c);
+ println!("{:}", p.get_next_level_protocol(),);
+ }
+ fn ipv4_drop(&self, p: &Ipv4Packet, c: &ClientInfo) {
+ self.prolog("ipv4", "drop", false);
+ self.client_info(c);
+ println!("{:}", p.get_next_level_protocol(),);
+ }
+ fn ipv4_send(&self, p: &MutableIpv4Packet, c: &ClientInfo) {
+ self.prolog("ipv4", "send", false);
+ self.client_info(c);
+ println!("{:}", p.get_next_level_protocol(),);
+ }
+ /* IPv6 */
+ fn ipv6_enabled(&self) -> bool {
+ self.ipv6
+ }
+ fn ipv6_recv(&self, p: &Ipv6Packet, c: &ClientInfo) {
+ self.prolog("ipv6", "recv", false);
+ self.client_info(c);
+ println!("{:}", p.get_next_header(),);
+ }
+ fn ipv6_drop(&self, p: &Ipv6Packet, c: &ClientInfo) {
+ self.prolog("ipv6", "drop", false);
+ self.client_info(c);
+ println!("{:}", p.get_next_header(),);
+ }
+ fn ipv6_send(&self, p: &MutableIpv6Packet, c: &ClientInfo) {
+ self.prolog("ipv6", "send", false);
+ self.client_info(c);
+ println!("{:}", p.get_next_header(),);
+ }
+ /* ICMPv4 */
+ fn icmpv4_enabled(&self) -> bool {
+ self.icmpv4
+ }
+ fn icmpv4_recv(&self, p: &IcmpPacket, c: &ClientInfo) {
+ self.prolog("icmpv4", "recv", false);
+ self.client_info(c);
+ println!("{:?}\t{:?}", p.get_icmp_type(), p.get_icmp_code(),);
+ }
+ fn icmpv4_drop(&self, p: &IcmpPacket, c: &ClientInfo) {
+ self.prolog("icmpv4", "drop", false);
+ self.client_info(c);
+ println!("{:?}\t{:?}", p.get_icmp_type(), p.get_icmp_code(),);
+ }
+ fn icmpv4_send(&self, p: &MutableIcmpPacket, c: &ClientInfo) {
+ self.prolog("icmpv4", "send", false);
+ self.client_info(c);
+ println!("{:?}\t{:?}", p.get_icmp_type(), p.get_icmp_code(),);
+ }
+ /* ICMPv6 */
+ fn icmpv6_enabled(&self) -> bool {
+ self.icmpv6
+ }
+ fn icmpv6_recv(&self, p: &Icmpv6Packet, c: &ClientInfo) {
+ self.prolog("icmpv6", "recv", false);
+ self.client_info(c);
+ println!("{:?}\t{:?}", p.get_icmpv6_type(), p.get_icmpv6_code(),);
+ }
+ fn icmpv6_drop(&self, p: &Icmpv6Packet, c: &ClientInfo) {
+ self.prolog("icmpv6", "drop", false);
+ self.client_info(c);
+ println!("{:?}\t{:?}", p.get_icmpv6_type(), p.get_icmpv6_code(),);
+ }
+ fn icmpv6_send(&self, p: &MutableIcmpv6Packet, c: &ClientInfo) {
+ self.prolog("icmpv6", "send", false);
+ self.client_info(c);
+ println!("{:?}\t{:?}", p.get_icmpv6_type(), p.get_icmpv6_code(),);
+ }
+ /* TCP */
+ fn tcp_enabled(&self) -> bool {
+ self.tcp
+ }
+ fn tcp_recv(&self, p: &TcpPacket, c: &ClientInfo) {
+ self.prolog("tcp", "recv", false);
+ self.client_info(c);
+ println!(
+ "{:?}\t{:}\t{:}",
+ p.get_flags(),
+ p.get_sequence(),
+ p.get_acknowledgement(),
+ );
+ }
+ fn tcp_drop(&self, p: &TcpPacket, c: &ClientInfo) {
+ self.prolog("tcp", "drop", false);
+ self.client_info(c);
+ println!(
+ "{:?}\t{:}\t{:}",
+ p.get_flags(),
+ p.get_sequence(),
+ p.get_acknowledgement(),
+ );
+ }
+ fn tcp_send(&self, p: &MutableTcpPacket, c: &ClientInfo) {
+ self.prolog("tcp", "send", false);
+ self.client_info(c);
+ println!(
+ "{:?}\t{:}\t{:}",
+ p.get_flags(),
+ p.get_sequence(),
+ p.get_acknowledgement(),
+ );
+ }
+ /* UDP */
+ fn udp_enabled(&self) -> bool {
+ self.udp
+ }
+ fn udp_recv(&self, _p: &UdpPacket, c: &ClientInfo) {
+ self.prolog("udp", "recv", false);
+ self.client_info(c);
+ println!("");
+ }
+ fn udp_drop(&self, _p: &UdpPacket, c: &ClientInfo) {
+ self.prolog("udp", "drop", false);
+ self.client_info(c);
+ println!("");
+ }
+ fn udp_send(&self, _p: &MutableUdpPacket, c: &ClientInfo) {
+ self.prolog("udp", "send", false);
+ self.client_info(c);
+ println!("");
+ }
+}
diff --git a/src/logger/logfmt.rs b/src/logger/logfmt.rs
new file mode 100644
index 0000000..5c3e02a
--- /dev/null
+++ b/src/logger/logfmt.rs
@@ -0,0 +1,332 @@
+// This file is part of masscanned.
+// Copyright 2021 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use std::time::SystemTime;
+
+use pnet::packet::{
+ arp::{ArpPacket, MutableArpPacket},
+ ethernet::{EthernetPacket, MutableEthernetPacket},
+ icmp::{IcmpPacket, MutableIcmpPacket},
+ icmpv6::{Icmpv6Packet, MutableIcmpv6Packet},
+ ipv4::{Ipv4Packet, MutableIpv4Packet},
+ ipv6::{Ipv6Packet, MutableIpv6Packet},
+ tcp::{MutableTcpPacket, TcpPacket},
+ udp::{MutableUdpPacket, UdpPacket},
+};
+
+use crate::client::ClientInfo;
+use crate::logger::Logger;
+
+pub struct LogfmtLogger {
+ arp: bool,
+ eth: bool,
+ ipv4: bool,
+ ipv6: bool,
+ icmpv4: bool,
+ icmpv6: bool,
+ tcp: bool,
+ udp: bool,
+}
+
+impl LogfmtLogger {
+ pub fn new() -> Self {
+ LogfmtLogger {
+ arp: true,
+ eth: true,
+ ipv4: true,
+ ipv6: true,
+ icmpv4: true,
+ icmpv6: true,
+ tcp: true,
+ udp: true,
+ }
+ }
+ fn prolog(&self, proto: &str, verb: &str, crlf: bool) {
+ let now = SystemTime::now()
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .unwrap();
+ print!(
+ "ts={}.{} proto={} verb={}{}",
+ now.as_secs(),
+ now.subsec_millis(),
+ proto,
+ verb,
+ if crlf { "\n" } else { " " },
+ );
+ }
+ fn client_info(&self, c: &ClientInfo) {
+ print!(
+ "{}{}{}{}{}{}{}",
+ if let Some(m) = c.mac.src {
+ format!(" mac_src={}", m)
+ } else {
+ "".to_string()
+ },
+ if let Some(m) = c.mac.dst {
+ format!(" mac_dst={}", m)
+ } else {
+ "".to_string()
+ },
+ if let Some(i) = c.ip.src {
+ format!(" ip_src={}", i)
+ } else {
+ "".to_string()
+ },
+ if let Some(i) = c.ip.dst {
+ format!(" ip_dst={}", i)
+ } else {
+ "".to_string()
+ },
+ if let Some(t) = c.transport {
+ format!(" transport={}", t)
+ } else {
+ "".to_string()
+ },
+ if let Some(p) = c.port.src {
+ format!(" port_src={}", p)
+ } else {
+ "".to_string()
+ },
+ if let Some(p) = c.port.dst {
+ format!(" port_dst={}", p)
+ } else {
+ "".to_string()
+ },
+ );
+ }
+}
+
+impl Logger for LogfmtLogger {
+ fn init(&self) {
+ self.prolog("arp", "init", true);
+ self.prolog("eth", "init", true);
+ self.prolog("ipv4", "init", true);
+ self.prolog("ipv6", "init", true);
+ self.prolog("icmpv4", "init", true);
+ self.prolog("icmpv6", "init", true);
+ self.prolog("tcp", "init", true);
+ self.prolog("udp", "init", true);
+ }
+ /* ARP */
+ fn arp_enabled(&self) -> bool {
+ self.arp
+ }
+ fn arp_recv(&self, p: &ArpPacket) {
+ self.prolog("arp", "recv", false);
+ println!(
+ " mac_src={:} mac_dst={:} ip_src={:} ip_dst={:} op={:?}",
+ p.get_sender_hw_addr(),
+ p.get_target_hw_addr(),
+ p.get_sender_proto_addr(),
+ p.get_target_proto_addr(),
+ p.get_operation(),
+ );
+ }
+ fn arp_drop(&self, p: &ArpPacket) {
+ self.prolog("arp", "drop", false);
+ println!(
+ " mac_src={:} mac_dst={:} ip_src={:} ip_dst={:} op={:?}",
+ p.get_sender_hw_addr(),
+ p.get_target_hw_addr(),
+ p.get_sender_proto_addr(),
+ p.get_target_proto_addr(),
+ p.get_operation(),
+ );
+ }
+ fn arp_send(&self, p: &MutableArpPacket) {
+ self.prolog("arp", "send", false);
+ println!(
+ " mac_dst={:} mac_src={:} ip_dst={:} ip_src={:} op={:?}",
+ p.get_target_hw_addr(),
+ p.get_sender_hw_addr(),
+ p.get_target_proto_addr(),
+ p.get_sender_proto_addr(),
+ p.get_operation(),
+ );
+ }
+ /* Ethernet */
+ fn eth_enabled(&self) -> bool {
+ self.eth
+ }
+ fn eth_recv(&self, p: &EthernetPacket, c: &ClientInfo) {
+ self.prolog("eth", "recv", false);
+ self.client_info(c);
+ println!(" eth_type={:}", p.get_ethertype(),);
+ }
+ fn eth_drop(&self, p: &EthernetPacket, c: &ClientInfo) {
+ self.prolog("eth", "drop", false);
+ self.client_info(c);
+ println!(" eth_type={:}", p.get_ethertype(),);
+ }
+ fn eth_send(&self, p: &MutableEthernetPacket, c: &ClientInfo) {
+ self.prolog("eth", "send", false);
+ self.client_info(c);
+ println!(" eth_type={:}", p.get_ethertype(),);
+ }
+ /* IPv4 */
+ fn ipv4_enabled(&self) -> bool {
+ self.ipv4
+ }
+ fn ipv4_recv(&self, p: &Ipv4Packet, c: &ClientInfo) {
+ self.prolog("ipv4", "recv", false);
+ self.client_info(c);
+ println!(" next_proto={:}", p.get_next_level_protocol(),);
+ }
+ fn ipv4_drop(&self, p: &Ipv4Packet, c: &ClientInfo) {
+ self.prolog("ipv4", "drop", false);
+ self.client_info(c);
+ println!(" next_proto={:}", p.get_next_level_protocol(),);
+ }
+ fn ipv4_send(&self, p: &MutableIpv4Packet, c: &ClientInfo) {
+ self.prolog("ipv4", "send", false);
+ self.client_info(c);
+ println!(" next_proto={:}", p.get_next_level_protocol(),);
+ }
+ /* IPv6 */
+ fn ipv6_enabled(&self) -> bool {
+ self.ipv6
+ }
+ fn ipv6_recv(&self, p: &Ipv6Packet, c: &ClientInfo) {
+ self.prolog("ipv6", "recv", false);
+ self.client_info(c);
+ println!(" next_proto={:}", p.get_next_header(),);
+ }
+ fn ipv6_drop(&self, p: &Ipv6Packet, c: &ClientInfo) {
+ self.prolog("ipv6", "drop", false);
+ self.client_info(c);
+ println!(" next_proto={:}", p.get_next_header(),);
+ }
+ fn ipv6_send(&self, p: &MutableIpv6Packet, c: &ClientInfo) {
+ self.prolog("ipv6", "send", false);
+ self.client_info(c);
+ println!(" next_proto={:}", p.get_next_header(),);
+ }
+ /* ICMPv4 */
+ fn icmpv4_enabled(&self) -> bool {
+ self.icmpv4
+ }
+ fn icmpv4_recv(&self, p: &IcmpPacket, c: &ClientInfo) {
+ self.prolog("icmpv4", "recv", false);
+ self.client_info(c);
+ println!(
+ " icmp_type={:?} icmp_code={:?}",
+ p.get_icmp_type(),
+ p.get_icmp_code(),
+ );
+ }
+ fn icmpv4_drop(&self, p: &IcmpPacket, c: &ClientInfo) {
+ self.prolog("icmpv4", "drop", false);
+ self.client_info(c);
+ println!(
+ " icmp_type={:?} icmp_code={:?}",
+ p.get_icmp_type(),
+ p.get_icmp_code(),
+ );
+ }
+ fn icmpv4_send(&self, p: &MutableIcmpPacket, c: &ClientInfo) {
+ self.prolog("icmpv4", "send", false);
+ self.client_info(c);
+ println!(
+ " icmp_type={:?} icmp_code={:?}",
+ p.get_icmp_type(),
+ p.get_icmp_code(),
+ );
+ }
+ /* ICMPv6 */
+ fn icmpv6_enabled(&self) -> bool {
+ self.icmpv6
+ }
+ fn icmpv6_recv(&self, p: &Icmpv6Packet, c: &ClientInfo) {
+ self.prolog("icmpv6", "recv", false);
+ self.client_info(c);
+ println!(
+ " icmpv6_type={:?} icmpv6_code={:?}",
+ p.get_icmpv6_type(),
+ p.get_icmpv6_code(),
+ );
+ }
+ fn icmpv6_drop(&self, p: &Icmpv6Packet, c: &ClientInfo) {
+ self.prolog("icmpv6", "drop", false);
+ self.client_info(c);
+ println!(
+ " icmpv6_type={:?} icmpv6_code={:?}",
+ p.get_icmpv6_type(),
+ p.get_icmpv6_code(),
+ );
+ }
+ fn icmpv6_send(&self, p: &MutableIcmpv6Packet, c: &ClientInfo) {
+ self.prolog("icmpv6", "send", false);
+ self.client_info(c);
+ println!(
+ " icmpv6_type={:?} icmpv6_code={:?}",
+ p.get_icmpv6_type(),
+ p.get_icmpv6_code(),
+ );
+ }
+ /* TCP */
+ fn tcp_enabled(&self) -> bool {
+ self.tcp
+ }
+ fn tcp_recv(&self, p: &TcpPacket, c: &ClientInfo) {
+ self.prolog("tcp", "recv", false);
+ self.client_info(c);
+ println!(
+ " flags={:?} seq={:} ack={:}",
+ p.get_flags(),
+ p.get_sequence(),
+ p.get_acknowledgement(),
+ );
+ }
+ fn tcp_drop(&self, p: &TcpPacket, c: &ClientInfo) {
+ self.prolog("tcp", "drop", false);
+ self.client_info(c);
+ println!(
+ " flags={:?} seq={:} ack={:}",
+ p.get_flags(),
+ p.get_sequence(),
+ p.get_acknowledgement(),
+ );
+ }
+ fn tcp_send(&self, p: &MutableTcpPacket, c: &ClientInfo) {
+ self.prolog("tcp", "send", false);
+ self.client_info(c);
+ println!(
+ " flags={:?} seq={:} ack={:}",
+ p.get_flags(),
+ p.get_sequence(),
+ p.get_acknowledgement(),
+ );
+ }
+ /* UDP */
+ fn udp_enabled(&self) -> bool {
+ self.udp
+ }
+ fn udp_recv(&self, _p: &UdpPacket, c: &ClientInfo) {
+ self.prolog("udp", "recv", false);
+ self.client_info(c);
+ println!("");
+ }
+ fn udp_drop(&self, _p: &UdpPacket, c: &ClientInfo) {
+ self.prolog("udp", "drop", false);
+ self.client_info(c);
+ println!("");
+ }
+ fn udp_send(&self, _p: &MutableUdpPacket, c: &ClientInfo) {
+ self.prolog("udp", "send", false);
+ self.client_info(c);
+ println!("");
+ }
+}
diff --git a/src/logger/meta.rs b/src/logger/meta.rs
new file mode 100644
index 0000000..94c752b
--- /dev/null
+++ b/src/logger/meta.rs
@@ -0,0 +1,225 @@
+// This file is part of masscanned.
+// Copyright 2021 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use pnet::packet::{
+ arp::{ArpPacket, MutableArpPacket},
+ ethernet::{EthernetPacket, MutableEthernetPacket},
+ icmp::{IcmpPacket, MutableIcmpPacket},
+ icmpv6::{Icmpv6Packet, MutableIcmpv6Packet},
+ ipv4::{Ipv4Packet, MutableIpv4Packet},
+ ipv6::{Ipv6Packet, MutableIpv6Packet},
+ tcp::{MutableTcpPacket, TcpPacket},
+ udp::{MutableUdpPacket, UdpPacket},
+};
+
+use crate::client::ClientInfo;
+use crate::logger::Logger;
+
+pub struct MetaLogger {
+ loggers: Vec>,
+}
+
+impl MetaLogger {
+ pub fn new() -> Self {
+ MetaLogger {
+ loggers: Vec::new(),
+ }
+ }
+ pub fn add(&mut self, log: Box) {
+ self.loggers.push(log);
+ }
+ pub fn init(&self) {
+ for l in &self.loggers {
+ l.init();
+ }
+ }
+ /* ARP */
+ pub fn arp_recv(&self, p: &ArpPacket) {
+ for l in &self.loggers {
+ if l.arp_enabled() {
+ l.arp_recv(p);
+ }
+ }
+ }
+ pub fn arp_drop(&self, p: &ArpPacket) {
+ for l in &self.loggers {
+ if l.arp_enabled() {
+ l.arp_drop(p);
+ }
+ }
+ }
+ pub fn arp_send(&self, p: &MutableArpPacket) {
+ for l in &self.loggers {
+ if l.arp_enabled() {
+ l.arp_send(p);
+ }
+ }
+ }
+ /* Ethernet */
+ pub fn eth_recv(&self, p: &EthernetPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.eth_enabled() {
+ l.eth_recv(p, c);
+ }
+ }
+ }
+ pub fn eth_drop(&self, p: &EthernetPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.eth_enabled() {
+ l.eth_drop(p, c);
+ }
+ }
+ }
+ pub fn eth_send(&self, p: &MutableEthernetPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.eth_enabled() {
+ l.eth_send(p, c);
+ }
+ }
+ }
+ /* IPv4 */
+ pub fn ipv4_recv(&self, p: &Ipv4Packet, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.ipv4_enabled() {
+ l.ipv4_recv(p, c);
+ }
+ }
+ }
+ pub fn ipv4_drop(&self, p: &Ipv4Packet, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.ipv4_enabled() {
+ l.ipv4_drop(p, c);
+ }
+ }
+ }
+ pub fn ipv4_send(&self, p: &MutableIpv4Packet, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.ipv4_enabled() {
+ l.ipv4_send(p, c);
+ }
+ }
+ }
+ /* IPv6 */
+ pub fn ipv6_recv(&self, p: &Ipv6Packet, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.ipv6_enabled() {
+ l.ipv6_recv(p, c);
+ }
+ }
+ }
+ pub fn ipv6_drop(&self, p: &Ipv6Packet, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.ipv6_enabled() {
+ l.ipv6_drop(p, c);
+ }
+ }
+ }
+ pub fn ipv6_send(&self, p: &MutableIpv6Packet, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.ipv6_enabled() {
+ l.ipv6_send(p, c);
+ }
+ }
+ }
+ /* ICMPv4 */
+ pub fn icmpv4_recv(&self, p: &IcmpPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.icmpv4_enabled() {
+ l.icmpv4_recv(p, c);
+ }
+ }
+ }
+ pub fn icmpv4_drop(&self, p: &IcmpPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.icmpv4_enabled() {
+ l.icmpv4_drop(p, c);
+ }
+ }
+ }
+ pub fn icmpv4_send(&self, p: &MutableIcmpPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.icmpv4_enabled() {
+ l.icmpv4_send(p, c);
+ }
+ }
+ }
+ /* ICMPv6 */
+ pub fn icmpv6_recv(&self, p: &Icmpv6Packet, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.icmpv6_enabled() {
+ l.icmpv6_recv(p, c);
+ }
+ }
+ }
+ pub fn icmpv6_drop(&self, p: &Icmpv6Packet, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.icmpv6_enabled() {
+ l.icmpv6_drop(p, c);
+ }
+ }
+ }
+ pub fn icmpv6_send(&self, p: &MutableIcmpv6Packet, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.icmpv6_enabled() {
+ l.icmpv6_send(p, c);
+ }
+ }
+ }
+ /* TCP */
+ pub fn tcp_recv(&self, p: &TcpPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.tcp_enabled() {
+ l.tcp_recv(p, c);
+ }
+ }
+ }
+ pub fn tcp_drop(&self, p: &TcpPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.tcp_enabled() {
+ l.tcp_drop(p, c);
+ }
+ }
+ }
+ pub fn tcp_send(&self, p: &MutableTcpPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.tcp_enabled() {
+ l.tcp_send(p, c);
+ }
+ }
+ }
+ /* UDP */
+ pub fn udp_recv(&self, p: &UdpPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.udp_enabled() {
+ l.udp_recv(p, c);
+ }
+ }
+ }
+ pub fn udp_drop(&self, p: &UdpPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.udp_enabled() {
+ l.udp_drop(p, c);
+ }
+ }
+ }
+ pub fn udp_send(&self, p: &MutableUdpPacket, c: &ClientInfo) {
+ for l in &self.loggers {
+ if l.udp_enabled() {
+ l.udp_send(p, c);
+ }
+ }
+ }
+}
diff --git a/src/logger/mod.rs b/src/logger/mod.rs
new file mode 100644
index 0000000..8a6fb5e
--- /dev/null
+++ b/src/logger/mod.rs
@@ -0,0 +1,97 @@
+// This file is part of masscanned.
+// Copyright 2021 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use pnet::packet::{
+ arp::{ArpPacket, MutableArpPacket},
+ ethernet::{EthernetPacket, MutableEthernetPacket},
+ icmp::{IcmpPacket, MutableIcmpPacket},
+ icmpv6::{Icmpv6Packet, MutableIcmpv6Packet},
+ ipv4::{Ipv4Packet, MutableIpv4Packet},
+ ipv6::{Ipv6Packet, MutableIpv6Packet},
+ tcp::{MutableTcpPacket, TcpPacket},
+ udp::{MutableUdpPacket, UdpPacket},
+};
+
+use crate::client::ClientInfo;
+
+mod console;
+mod logfmt;
+mod meta;
+
+pub use console::ConsoleLogger;
+pub use logfmt::LogfmtLogger;
+pub use meta::MetaLogger;
+
+pub trait Logger {
+ fn init(&self);
+ /* list of notifications that a logger might or might not implement */
+ /* ARP */
+ fn arp_enabled(&self) -> bool {
+ true
+ }
+ fn arp_recv(&self, _p: &ArpPacket) {}
+ fn arp_drop(&self, _p: &ArpPacket) {}
+ fn arp_send(&self, _p: &MutableArpPacket) {}
+ /* Ethernet */
+ fn eth_enabled(&self) -> bool {
+ true
+ }
+ fn eth_recv(&self, _p: &EthernetPacket, _c: &ClientInfo) {}
+ fn eth_drop(&self, _p: &EthernetPacket, _c: &ClientInfo) {}
+ fn eth_send(&self, _p: &MutableEthernetPacket, _c: &ClientInfo) {}
+ /* IPv4 */
+ fn ipv4_enabled(&self) -> bool {
+ true
+ }
+ fn ipv4_recv(&self, _p: &Ipv4Packet, _c: &ClientInfo) {}
+ fn ipv4_drop(&self, _p: &Ipv4Packet, _c: &ClientInfo) {}
+ fn ipv4_send(&self, _p: &MutableIpv4Packet, _c: &ClientInfo) {}
+ /* IPv6 */
+ fn ipv6_enabled(&self) -> bool {
+ true
+ }
+ fn ipv6_recv(&self, _p: &Ipv6Packet, _c: &ClientInfo) {}
+ fn ipv6_drop(&self, _p: &Ipv6Packet, _c: &ClientInfo) {}
+ fn ipv6_send(&self, _p: &MutableIpv6Packet, _c: &ClientInfo) {}
+ /* ICMPv4 */
+ fn icmpv4_enabled(&self) -> bool {
+ true
+ }
+ fn icmpv4_recv(&self, _p: &IcmpPacket, _c: &ClientInfo) {}
+ fn icmpv4_drop(&self, _p: &IcmpPacket, _c: &ClientInfo) {}
+ fn icmpv4_send(&self, _p: &MutableIcmpPacket, _c: &ClientInfo) {}
+ /* ICMPv6 */
+ fn icmpv6_enabled(&self) -> bool {
+ true
+ }
+ fn icmpv6_recv(&self, _p: &Icmpv6Packet, _c: &ClientInfo) {}
+ fn icmpv6_drop(&self, _p: &Icmpv6Packet, _c: &ClientInfo) {}
+ fn icmpv6_send(&self, _p: &MutableIcmpv6Packet, _c: &ClientInfo) {}
+ /* TCP */
+ fn tcp_enabled(&self) -> bool {
+ true
+ }
+ fn tcp_recv(&self, _p: &TcpPacket, _c: &ClientInfo) {}
+ fn tcp_drop(&self, _p: &TcpPacket, _c: &ClientInfo) {}
+ fn tcp_send(&self, _p: &MutableTcpPacket, _c: &ClientInfo) {}
+ /* UDP */
+ fn udp_enabled(&self) -> bool {
+ true
+ }
+ fn udp_recv(&self, _p: &UdpPacket, _c: &ClientInfo) {}
+ fn udp_drop(&self, _p: &UdpPacket, _c: &ClientInfo) {}
+ fn udp_send(&self, _p: &MutableUdpPacket, _c: &ClientInfo) {}
+}
diff --git a/src/masscanned.rs b/src/masscanned.rs
index 5794357..5198dec 100644
--- a/src/masscanned.rs
+++ b/src/masscanned.rs
@@ -1,5 +1,5 @@
// This file is part of masscanned.
-// Copyright 2021 - The IVRE project
+// Copyright 2021 - 2022 The IVRE project
//
// Masscanned is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
@@ -24,7 +24,7 @@ use std::fs::File;
use std::net::IpAddr;
use std::str::FromStr;
-use clap::{App, Arg};
+use clap::{builder::PossibleValuesParser, Arg, ArgAction, Command};
use log::*;
use pnet::{
datalink::{self, Channel::Ethernet, DataLinkReceiver, DataLinkSender, NetworkInterface},
@@ -35,12 +35,14 @@ use pnet::{
util::MacAddr,
};
+use crate::logger::{ConsoleLogger, LogfmtLogger, Logger, MetaLogger};
use crate::utils::IpAddrParser;
mod client;
mod layer_2;
mod layer_3;
mod layer_4;
+mod logger;
mod proto;
mod smack;
mod synackcookie;
@@ -54,7 +56,10 @@ pub struct Masscanned<'a> {
pub mac: MacAddr,
/* iface is an Option to make tests easier */
pub iface: Option<&'a NetworkInterface>,
- pub ip_addresses: Option<&'a HashSet>,
+ pub self_ip_list: Option<&'a HashSet>,
+ pub remote_ip_deny_list: Option<&'a HashSet>,
+ /* loggers */
+ pub log: MetaLogger,
}
/* Get the L2 network interface from its name */
@@ -98,40 +103,75 @@ fn reply<'a, 'b>(packet: &'a [u8], masscanned: &Masscanned) -> Option("interface")
.expect("error parsing iface argument"),
) {
i
} else {
error!(
"Cannot open interface \"{}\" - are you sure it exists?",
- args.value_of("interface")
+ args.get_one::("interface")
.expect("error parsing iface argument")
);
return;
};
- if iface.flags & (netdevice::IFF_UP.bits() as u32) == 0 {
+ if !iface.is_up() {
error!("specified interface is DOWN");
return;
}
- let mac = if let Some(m) = args.value_of("mac") {
+ let mac = if let Some(m) = args.get_one::("mac") {
MacAddr::from_str(m).expect("error parsing provided MAC address")
} else if let Some(m) = iface.mac {
m
@@ -172,9 +209,9 @@ fn main() {
};
/* Parse ip address file specified */
/* FIXME: .and_then(|path| File::open(path).map(|file| )).unwrap_or_default() ? */
- let ip_list = if let Some(ref path) = args.value_of("ip") {
+ let mut ip_list = if let Some(ref path) = args.get_one::("selfipfile") {
if let Ok(file) = File::open(path) {
- info!("parsing ip address file: {}", &path);
+ info!("parsing self ip file: {}", &path);
file.extract_ip_addresses_only(None)
} else {
HashSet::new()
@@ -182,23 +219,74 @@ fn main() {
} else {
HashSet::new()
};
- let ip_addresses = if !ip_list.is_empty() {
+ if let Some(ip_inline) = args.get_one::("selfiplist") {
+ ip_list.extend(ip_inline.extract_ip_addresses_only(None));
+ }
+ let self_ip_list = if !ip_list.is_empty() {
+ for ip in &ip_list {
+ info!("binding........{}", ip);
+ }
+ Some(&ip_list)
+ } else {
+ info!("binding........0.0.0.0");
+ info!("binding........::");
+ None
+ };
+ /* Parse remote ip deny file specified */
+ let mut ip_list = if let Some(ref path) = args.get_one::("remoteipdenyfile") {
+ if let Ok(file) = File::open(path) {
+ info!("parsing remote ip deny file: {}", &path);
+ file.extract_ip_addresses_only(None)
+ } else {
+ HashSet::new()
+ }
+ } else {
+ HashSet::new()
+ };
+ if let Some(ip_inline) = args.get_one::("remoteipdenylist") {
+ ip_list.extend(ip_inline.extract_ip_addresses_only(None));
+ }
+ let remote_ip_deny_list = if !ip_list.is_empty() {
+ for ip in &ip_list {
+ info!("ignoring.......{}", ip);
+ }
Some(&ip_list)
} else {
None
};
- let masscanned = Masscanned {
+
+ let mut masscanned = Masscanned {
synack_key: [0, 0],
mac,
iface: Some(&iface),
- ip_addresses,
+ self_ip_list,
+ remote_ip_deny_list,
+ log: MetaLogger::new(),
};
info!("interface......{}", masscanned.iface.unwrap().name);
info!("mac address....{}", masscanned.mac);
+ if !args
+ .get_one::("quiet")
+ .expect("unexpected error parsing argument")
+ {
+ if let Some(format) = args.get_one::("format") {
+ let chosen_logger: Box = match format.as_str() {
+ "console" => Box::new(ConsoleLogger::new()),
+ "logfmt" => Box::new(LogfmtLogger::new()),
+
+ // clap should already ensure we're using a valid format
+ _ => panic!("illegal format"),
+ };
+ masscanned.log.add(chosen_logger);
+ } else {
+ masscanned.log.add(Box::new(ConsoleLogger::new()));
+ }
+ masscanned.log.init();
+ }
let (mut tx, mut rx) = get_channel(masscanned.iface.unwrap());
loop {
/* check if network interface is still up */
- if masscanned.iface.unwrap().flags & (netdevice::IFF_UP.bits() as u32) == 0 {
+ if !masscanned.iface.unwrap().is_up() {
error!("interface is DOWN - aborting");
break;
}
diff --git a/src/proto/dissector.rs b/src/proto/dissector.rs
new file mode 100644
index 0000000..a370e12
--- /dev/null
+++ b/src/proto/dissector.rs
@@ -0,0 +1,92 @@
+// This file is part of masscanned.
+// Copyright 2021 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use crate::proto::ClientInfo;
+use crate::proto::TCPControlBlock;
+use crate::Masscanned;
+
+////////////
+// Common //
+////////////
+
+/// ### PacketDissector
+/// A util class used to dissect fields.
+#[derive(Debug, Clone)]
+pub struct PacketDissector {
+ pub i: usize,
+ pub state: T,
+}
+
+impl PacketDissector {
+ pub fn new(initial_state: T) -> PacketDissector {
+ return PacketDissector {
+ i: 0,
+ state: initial_state,
+ };
+ }
+ pub fn next_state(&mut self, state: T) {
+ self.state = state;
+ self.i = 0;
+ }
+ pub fn next_state_when_i_reaches(&mut self, state: T, i: usize) {
+ if self.i == i {
+ self.next_state(state);
+ }
+ }
+ fn _read_usize(&mut self, byte: &u8, value: usize, next_state: T, size: usize) -> usize {
+ self.i += 1;
+ self.next_state_when_i_reaches(next_state, size);
+ (value << 8) + *byte as usize
+ }
+ fn _read_ulesize(&mut self, byte: &u8, value: usize, next_state: T, size: usize) -> usize {
+ let ret = value + ((*byte as usize) << (8 * self.i));
+ self.i += 1;
+ self.next_state_when_i_reaches(next_state, size);
+ ret
+ }
+ pub fn read_u16(&mut self, byte: &u8, value: u16, next_state: T) -> u16 {
+ self._read_usize(byte, value as usize, next_state, 2) as u16
+ }
+ pub fn read_ule16(&mut self, byte: &u8, value: u16, next_state: T) -> u16 {
+ self._read_ulesize(byte, value as usize, next_state, 2) as u16
+ }
+ pub fn read_u32(&mut self, byte: &u8, value: u32, next_state: T) -> u32 {
+ self._read_usize(byte, value as usize, next_state, 4) as u32
+ }
+ pub fn read_ule32(&mut self, byte: &u8, value: u32, next_state: T) -> u32 {
+ self._read_ulesize(byte, value as usize, next_state, 4) as u32
+ }
+ pub fn read_ule64(&mut self, byte: &u8, value: u64, next_state: T) -> u64 {
+ self._read_ulesize(byte, value as usize, next_state, 8) as u64
+ }
+}
+
+pub trait MPacket {
+ fn new() -> Self;
+ fn repl(
+ &self,
+ _masscanned: &Masscanned,
+ _client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+ ) -> Option>;
+ fn parse(&mut self, byte: &u8);
+
+ fn parse_all(&mut self, bytes: &[u8]) {
+ for byte in bytes {
+ self.parse(byte);
+ }
+ }
+}
diff --git a/src/proto/dns/cst.rs b/src/proto/dns/cst.rs
new file mode 100644
index 0000000..ff306c4
--- /dev/null
+++ b/src/proto/dns/cst.rs
@@ -0,0 +1,93 @@
+// This file is part of masscanned.
+// Copyright 2022 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use strum_macros::EnumIter;
+
+#[derive(PartialEq, Debug, Clone, Copy, EnumIter)]
+pub enum DNSType {
+ NONE,
+ A,
+ TXT, // value: 16 - text strings
+}
+
+impl From for DNSType {
+ fn from(item: u16) -> Self {
+ match item {
+ 1 => DNSType::A,
+ 16 => DNSType::TXT,
+ _ => DNSType::NONE,
+ }
+ }
+}
+
+impl From for u16 {
+ fn from(item: DNSType) -> Self {
+ match item {
+ DNSType::A => 1,
+ DNSType::TXT => 16,
+ _ => 0,
+ }
+ }
+}
+
+#[derive(PartialEq, Debug, Clone, Copy, EnumIter)]
+pub enum DNSClass {
+ NONE,
+ IN, // value: 1 - the Internet
+ CH, // value: 3 - the CHAOS class
+}
+
+impl From for DNSClass {
+ fn from(item: u16) -> Self {
+ match item {
+ 1 => DNSClass::IN,
+ 3 => DNSClass::CH,
+ _ => DNSClass::NONE,
+ }
+ }
+}
+
+impl From for u16 {
+ fn from(item: DNSClass) -> Self {
+ match item {
+ DNSClass::IN => 1,
+ DNSClass::CH => 3,
+ _ => 0,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn type_parse() {
+ /* type TXT */
+ assert!(DNSType::from(1) == DNSType::A);
+ assert!(1 as u16 == DNSType::A.into());
+ assert!(DNSType::from(16) == DNSType::TXT);
+ assert!(16 as u16 == DNSType::TXT.into());
+ }
+
+ #[test]
+ fn class_parse() {
+ assert!(DNSClass::from(1) == DNSClass::IN);
+ assert!(1 as u16 == DNSClass::IN.into());
+ assert!(DNSClass::from(3) == DNSClass::CH);
+ assert!(3 as u16 == DNSClass::CH.into());
+ }
+}
diff --git a/src/proto/dns/header.rs b/src/proto/dns/header.rs
new file mode 100644
index 0000000..e8a8d6e
--- /dev/null
+++ b/src/proto/dns/header.rs
@@ -0,0 +1,387 @@
+// This file is part of masscanned.
+// Copyright 2022 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use std::convert::TryFrom;
+
+use crate::proto::dissector::{MPacket, PacketDissector};
+use crate::proto::ClientInfo;
+use crate::proto::TCPControlBlock;
+use crate::Masscanned;
+
+#[derive(PartialEq)]
+pub enum DNSHeaderState {
+ Id,
+ Flags,
+ QDCount,
+ ANCount,
+ NSCount,
+ ARCount,
+ End,
+}
+
+pub struct DNSHeader {
+ pub d: PacketDissector,
+ pub id: u16,
+ pub flags: u16,
+ pub _qr: bool,
+ pub _opcode: u8,
+ pub _aa: bool,
+ pub _tc: bool,
+ pub _rd: bool,
+ pub _ra: bool,
+ pub _z: u8,
+ pub _rcode: u8,
+ pub qdcount: u16,
+ pub ancount: u16,
+ pub nscount: u16,
+ pub arcount: u16,
+}
+
+impl TryFrom> for DNSHeader {
+ type Error = &'static str;
+
+ fn try_from(item: Vec) -> Result {
+ let mut hdr = DNSHeader::new();
+ for b in item {
+ hdr.parse(&b);
+ }
+ if hdr.d.state == DNSHeaderState::End {
+ Ok(hdr)
+ } else {
+ Err("packet is incomplete")
+ }
+ }
+}
+
+impl From<&DNSHeader> for Vec {
+ fn from(item: &DNSHeader) -> Self {
+ let mut v = Vec::new();
+ /* id */
+ v.push((item.id >> 8) as u8);
+ v.push((item.id & 0xFF) as u8);
+
+ /* flags */
+ /* QR | OPCODE | AA | TC | RD */
+ v.push(
+ ((item._qr as u8) << 7)
+ | (item._opcode << 3)
+ | ((item._aa as u8) << 2)
+ | ((item._tc as u8) << 1)
+ | (item._rd as u8),
+ );
+ /* AA | ZZZ | RCODE */
+ v.push(0);
+
+ /* qdcount */
+ v.push((item.qdcount >> 8) as u8);
+ v.push((item.qdcount & 0xFF) as u8);
+
+ /* ancount */
+ v.push((item.ancount >> 8) as u8);
+ v.push((item.ancount & 0xFF) as u8);
+
+ /* nscount */
+ v.push((item.nscount >> 8) as u8);
+ v.push((item.nscount & 0xFF) as u8);
+
+ /* arcount */
+ v.push((item.arcount >> 8) as u8);
+ v.push((item.arcount & 0xFF) as u8);
+
+ v
+ }
+}
+
+impl MPacket for DNSHeader {
+ fn new() -> Self {
+ DNSHeader {
+ d: PacketDissector::new(DNSHeaderState::Id),
+ id: 0,
+ flags: 0,
+ _qr: false,
+ _opcode: 0,
+ _aa: false,
+ _tc: false,
+ _rd: false,
+ _ra: false,
+ _z: 0,
+ _rcode: 0,
+ qdcount: 0,
+ ancount: 0,
+ nscount: 0,
+ arcount: 0,
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ DNSHeaderState::Id => {
+ self.id = self.d.read_u16(byte, self.id, DNSHeaderState::Flags);
+ }
+ DNSHeaderState::Flags => {
+ self.flags = self.d.read_u16(byte, self.flags, DNSHeaderState::QDCount);
+ }
+ DNSHeaderState::QDCount => {
+ self.qdcount = self.d.read_u16(byte, self.qdcount, DNSHeaderState::ANCount);
+ }
+ DNSHeaderState::ANCount => {
+ self.ancount = self.d.read_u16(byte, self.ancount, DNSHeaderState::NSCount);
+ }
+ DNSHeaderState::NSCount => {
+ self.nscount = self.d.read_u16(byte, self.nscount, DNSHeaderState::ARCount);
+ }
+ DNSHeaderState::ARCount => {
+ self.arcount = self.d.read_u16(byte, self.arcount, DNSHeaderState::End);
+ }
+ DNSHeaderState::End => {}
+ }
+ /* we need this to be executed at the same call
+ * the state changes to End, hence it is not in the
+ * match structure
+ **/
+ if self.d.state == DNSHeaderState::End {
+ self._qr = (self.flags >> 15) == 1;
+ self._opcode = ((self.flags >> 11) & 0x0F) as u8;
+ self._aa = (self.flags >> 10) & 0x01 == 1;
+ self._tc = (self.flags >> 9) & 0x01 == 1;
+ self._rd = (self.flags >> 8) & 0x01 == 1;
+ self._ra = (self.flags >> 7) & 0x01 == 1;
+ self._z = ((self.flags >> 4) & 0x07) as u8;
+ self._rcode = (self.flags & 0x0F) as u8;
+ }
+ }
+
+ fn repl(
+ &self,
+ _masscanned: &Masscanned,
+ _client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ let mut r = DNSHeader::new();
+ r.id = self.id;
+ r._qr = true;
+ r._opcode = self._opcode;
+ r._aa = true;
+ r._tc = false;
+ /* RFC1035
+ * Recursion Desired - this bit may be set in a query and
+ * is copied into the response. */
+ r._rd = self._rd;
+ r._ra = false;
+ r.qdcount = self.qdcount;
+ r.ancount = self.qdcount;
+ Some(Vec::::from(&r))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use pnet::util::MacAddr;
+ use std::str::FromStr;
+
+ use crate::logger::MetaLogger;
+
+ #[test]
+ fn parse_all() {
+ let payload = b"\xb3\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00";
+ let hdr = match DNSHeader::try_from(payload.to_vec()) {
+ Ok(_hdr) => _hdr,
+ Err(e) => panic!("error while parsing DNS header: {}", e),
+ };
+ assert!(hdr.d.state == DNSHeaderState::End);
+ assert!(hdr.id == 0xb307);
+ assert!(hdr.flags == 0x0100);
+ assert!(hdr._qr == false);
+ assert!(hdr._opcode == 0);
+ assert!(hdr._aa == false);
+ assert!(hdr._tc == false);
+ assert!(hdr._rd == true);
+ assert!(hdr._ra == false);
+ assert!(hdr._z == 0);
+ assert!(hdr._rcode == 0);
+ assert!(hdr.qdcount == 1);
+ assert!(hdr.ancount == 0);
+ assert!(hdr.nscount == 0);
+ assert!(hdr.arcount == 0);
+ assert!(Vec::::from(&hdr) == payload.to_vec());
+ /* KO */
+ let payload = b"\xb3\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00";
+ match DNSHeader::try_from(payload.to_vec()) {
+ Ok(_) => panic!("parsing should have failed"),
+ Err(_) => {}
+ };
+ }
+
+ #[test]
+ fn parse_byte_by_byte() {
+ /* OK */
+ let payload = b"\xb3\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00";
+ let mut hdr = DNSHeader::new();
+ for b in payload {
+ assert!(hdr.d.state != DNSHeaderState::End);
+ hdr.parse(b);
+ }
+ assert!(hdr.d.state == DNSHeaderState::End);
+ assert!(hdr.id == 0xb307);
+ assert!(hdr.flags == 0x0100);
+ assert!(hdr._qr == false);
+ assert!(hdr._opcode == 0);
+ assert!(hdr._aa == false);
+ assert!(hdr._tc == false);
+ assert!(hdr._rd == true);
+ assert!(hdr._ra == false);
+ assert!(hdr._z == 0);
+ assert!(hdr._rcode == 0);
+ assert!(hdr.qdcount == 1);
+ assert!(hdr.ancount == 0);
+ assert!(hdr.nscount == 0);
+ assert!(hdr.arcount == 0);
+ assert!(Vec::::from(&hdr) == payload.to_vec());
+ /* KO */
+ let payload = b"\xb3\x07\x01\x00\x00\x01\x00\x00\x00\x00\x00";
+ let mut hdr = DNSHeader::new();
+ for b in payload {
+ hdr.parse(b);
+ }
+ assert!(hdr.d.state != DNSHeaderState::End);
+ }
+
+ fn consistency_qd_rr(qd: &DNSHeader, rr: &DNSHeader) {
+ assert!(rr.id == qd.id);
+ assert!(rr._qr == true);
+ assert!(rr._opcode == qd._opcode);
+ assert!(rr._aa == true);
+ assert!(rr._tc == false);
+ assert!(rr._rd == qd._rd);
+ assert!(rr._ra == false);
+ assert!(rr._z == 0);
+ assert!(rr._rcode == 0);
+ /* check flags */
+ assert!(rr.flags >> 15 == rr._qr as u16);
+ assert!((rr.flags >> 11) & 0xF == rr._opcode as u16);
+ assert!((rr.flags >> 10) & 0x1 == rr._aa as u16);
+ assert!((rr.flags >> 9) & 0x1 == rr._tc as u16);
+ assert!((rr.flags >> 8) & 0x1 == rr._rd as u16);
+ assert!((rr.flags >> 7) & 0x1 == rr._ra as u16);
+ assert!((rr.flags >> 4) & 0x7 == rr._z as u16);
+ assert!(rr.flags & 0xF == rr._rcode as u16);
+ assert!(rr.qdcount == qd.qdcount);
+ assert!(rr.ancount == qd.qdcount);
+ assert!(rr.nscount == 0);
+ assert!(rr.arcount == 0);
+ }
+
+ #[test]
+ fn repl_id() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let client_info = ClientInfo::new();
+ let mut hdr = DNSHeader::new();
+ hdr._qr = false;
+ for id in [0x1234, 0x4321, 0xffff, 0x0, 0x1337] {
+ hdr.id = id;
+ let hdr_repl = if let Some(r) = hdr.repl(&masscanned, &client_info, None) {
+ DNSHeader::try_from(r).unwrap()
+ } else {
+ panic!("expected DNS header answer, got None");
+ };
+ consistency_qd_rr(&hdr, &hdr_repl);
+ }
+ }
+
+ #[test]
+ fn repl_opcode() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let client_info = ClientInfo::new();
+ let mut hdr = DNSHeader::new();
+ hdr._qr = false;
+ /* opcode */
+ for opcode in 0..3 {
+ hdr._opcode = opcode;
+ let hdr_repl = if let Some(r) = hdr.repl(&masscanned, &client_info, None) {
+ DNSHeader::try_from(r).unwrap()
+ } else {
+ panic!("expected DNS header answer, got None");
+ };
+ consistency_qd_rr(&hdr, &hdr_repl);
+ }
+ }
+
+ #[test]
+ fn repl_rd() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let client_info = ClientInfo::new();
+ let mut hdr = DNSHeader::new();
+ hdr._qr = false;
+ /* rd */
+ for rd in [false, true] {
+ hdr._rd = rd;
+ let hdr_repl = if let Some(r) = hdr.repl(&masscanned, &client_info, None) {
+ DNSHeader::try_from(r).unwrap()
+ } else {
+ panic!("expected DNS header answer, got None");
+ };
+ consistency_qd_rr(&hdr, &hdr_repl);
+ }
+ }
+
+ #[test]
+ fn repl_ancount() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let client_info = ClientInfo::new();
+ let mut hdr = DNSHeader::new();
+ hdr._qr = false;
+ /* rd */
+ for qdcount in 0..16 {
+ hdr.qdcount = qdcount;
+ let hdr_repl = if let Some(r) = hdr.repl(&masscanned, &client_info, None) {
+ DNSHeader::try_from(r).unwrap()
+ } else {
+ panic!("expected DNS header answer, got None");
+ };
+ consistency_qd_rr(&hdr, &hdr_repl);
+ }
+ }
+}
diff --git a/src/proto/dns/mod.rs b/src/proto/dns/mod.rs
new file mode 100644
index 0000000..0498791
--- /dev/null
+++ b/src/proto/dns/mod.rs
@@ -0,0 +1,688 @@
+// This file is part of masscanned.
+// Copyright 2022 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use std::convert::TryFrom;
+
+mod cst;
+
+mod header;
+use header::{DNSHeader, DNSHeaderState};
+
+mod query;
+use query::{DNSQuery, DNSQueryState};
+
+mod rr;
+use rr::{DNSRRState, DNSRR};
+
+use crate::proto::dissector::{MPacket, PacketDissector};
+use crate::proto::ClientInfo;
+use crate::proto::TCPControlBlock;
+use crate::Masscanned;
+
+#[derive(PartialEq, Debug)]
+enum DNSState {
+ Header,
+ Query,
+ Answer,
+ Authority,
+ Additional,
+ End,
+}
+
+pub struct DNSPacket {
+ d: PacketDissector,
+ header: DNSHeader,
+ qd: Vec,
+ rr: Vec,
+ ns: Vec,
+ ar: Vec,
+}
+
+impl TryFrom> for DNSPacket {
+ type Error = &'static str;
+
+ fn try_from(item: Vec) -> Result {
+ let mut dns = DNSPacket::new();
+ for b in item {
+ dns.parse(&b);
+ }
+ if dns.d.state == DNSState::End {
+ Ok(dns)
+ } else {
+ Err("packet is incomplete")
+ }
+ }
+}
+
+impl From<&DNSPacket> for Vec {
+ fn from(item: &DNSPacket) -> Self {
+ let mut v = Vec::new();
+ v.extend(Vec::::from(&item.header));
+ for qd in &item.qd {
+ v.extend(Vec::::from(qd));
+ }
+ for rr in &item.rr {
+ v.extend(Vec::::from(rr));
+ }
+ for ns in &item.ns {
+ v.extend(Vec::::from(ns));
+ }
+ for ar in &item.ar {
+ v.extend(Vec::::from(ar));
+ }
+ v
+ }
+}
+
+impl MPacket for DNSPacket {
+ fn new() -> Self {
+ DNSPacket {
+ d: PacketDissector::new(DNSState::Header),
+ header: DNSHeader::new(),
+ qd: Vec::new(),
+ rr: Vec::new(),
+ ns: Vec::new(),
+ ar: Vec::new(),
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ DNSState::Header => {
+ self.header.parse(byte);
+ if self.header.d.state == DNSHeaderState::End {
+ if self.header.qdcount > 0 {
+ self.qd.push(DNSQuery::new());
+ self.d.next_state(DNSState::Query);
+ } else if self.header.ancount > 0 {
+ self.rr.push(DNSRR::new());
+ self.d.next_state(DNSState::Answer);
+ } else if self.header.nscount > 0 {
+ self.d.next_state(DNSState::Authority);
+ } else if self.header.arcount > 0 {
+ self.d.next_state(DNSState::Additional);
+ } else {
+ self.d.next_state(DNSState::End);
+ }
+ }
+ }
+ DNSState::Query => {
+ let qdcount = self.qd.len();
+ self.qd[qdcount - 1].parse(byte);
+ if self.qd[qdcount - 1].d.state == DNSQueryState::End {
+ if self.header.qdcount as usize > self.qd.len() {
+ self.qd.push(DNSQuery::new());
+ } else if self.header.ancount > 0 {
+ self.rr.push(DNSRR::new());
+ self.d.next_state(DNSState::Answer);
+ } else if self.header.nscount > 0 {
+ self.d.next_state(DNSState::Authority);
+ } else if self.header.arcount > 0 {
+ self.d.next_state(DNSState::Additional);
+ } else {
+ self.d.next_state(DNSState::End);
+ }
+ }
+ }
+ DNSState::Answer => {
+ let ancount = self.rr.len();
+ self.rr[ancount - 1].parse(byte);
+ if self.rr[ancount - 1].d.state == DNSRRState::End {
+ if self.header.ancount as usize > self.rr.len() {
+ self.rr.push(DNSRR::new());
+ } else if self.header.nscount > 0 {
+ self.d.next_state(DNSState::Authority);
+ } else if self.header.arcount > 0 {
+ self.d.next_state(DNSState::Additional);
+ } else {
+ self.d.next_state(DNSState::End);
+ }
+ }
+ }
+ _ => {}
+ }
+ }
+
+ fn repl(
+ &self,
+ masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ let mut ans = DNSPacket::new();
+ ans.header = if let Some(hdr) = self.header.repl(&masscanned, &client_info, None) {
+ if let Ok(h) = DNSHeader::try_from(hdr) {
+ h
+ } else {
+ return None;
+ }
+ } else {
+ return None;
+ };
+ /* reply to qd */
+ for qd in &self.qd {
+ if let Ok(q) = DNSQuery::try_from(Vec::::from(qd)) {
+ ans.qd.push(q);
+ } else {
+ return None;
+ }
+ if let Some(raw_rr) = qd.repl(&masscanned, &client_info, None) {
+ if let Ok(rr) = DNSRR::try_from(raw_rr) {
+ ans.rr.push(rr);
+ } else {
+ return None;
+ }
+ } else {
+ return None;
+ }
+ }
+ Some(Vec::::from(&ans))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::cst::{DNSClass, DNSType};
+ use super::*;
+
+ use pnet::util::MacAddr;
+ use std::net::{IpAddr, Ipv4Addr};
+ use std::str::FromStr;
+
+ use crate::logger::MetaLogger;
+
+ #[test]
+ fn parse_qd_all() {
+ /* OK */
+ /* scapy: DNS(id=0x1337,
+ * qd=DNSQR(qname="www.example1.com")/DNSQR(qname="www.example2.com")/DNSQR(qname="www.example3.com"))
+ **/
+ let payload = b"\x137\x01\x00\x00\x03\x00\x00\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01";
+ let dns = match DNSPacket::try_from(payload.to_vec()) {
+ Ok(_dns) => _dns,
+ Err(e) => panic!("error while parsing DNS packet: {}", e),
+ };
+ assert!(dns.header.id == 0x1337);
+ assert!(dns.header._qr == false);
+ assert!(dns.header._opcode == 0);
+ assert!(dns.header._aa == false);
+ assert!(dns.header._tc == false);
+ assert!(dns.header._rd == true);
+ assert!(dns.header._ra == false);
+ assert!(dns.header._z == 0);
+ assert!(dns.header._rcode == 0);
+ assert!(dns.header.qdcount == 3);
+ assert!(dns.header.ancount == 0);
+ assert!(dns.header.nscount == 0);
+ assert!(dns.header.arcount == 0);
+ assert!(dns.qd.len() == 3);
+ assert!(
+ dns.qd[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[0].type_ == DNSType::A);
+ assert!(dns.qd[0].class == DNSClass::IN);
+ assert!(
+ dns.qd[1].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[1].type_ == DNSType::A);
+ assert!(dns.qd[1].class == DNSClass::IN);
+ assert!(
+ dns.qd[2].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[2].type_ == DNSType::A);
+ assert!(dns.qd[2].class == DNSClass::IN);
+ /* KO */
+ let payload = b"\x137\x01\x00\x00\x03\x00\x00\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00";
+ match DNSPacket::try_from(payload.to_vec()) {
+ Ok(_) => panic!("parsing should have failed"),
+ Err(_) => {}
+ }
+ let payload = b"xxx";
+ match DNSPacket::try_from(payload.to_vec()) {
+ Ok(_) => panic!("parsing should have failed"),
+ Err(_) => {}
+ }
+ }
+
+ #[test]
+ fn parse_qd_byte_by_byte() {
+ /* scapy: DNS(id=0x1337,
+ * qd=DNSQR(qname="www.example1.com")/DNSQR(qname="www.example2.com")/DNSQR(qname="www.example3.com"))
+ **/
+ let payload = b"\x137\x01\x00\x00\x03\x00\x00\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01";
+ let mut dns = DNSPacket::new();
+ for b in payload {
+ assert!(dns.d.state != DNSState::End);
+ dns.parse(&b);
+ }
+ assert!(dns.d.state == DNSState::End);
+ assert!(dns.header.id == 0x1337);
+ assert!(dns.header._qr == false);
+ assert!(dns.header._opcode == 0);
+ assert!(dns.header._aa == false);
+ assert!(dns.header._tc == false);
+ assert!(dns.header._rd == true);
+ assert!(dns.header._ra == false);
+ assert!(dns.header._z == 0);
+ assert!(dns.header._rcode == 0);
+ assert!(dns.header.qdcount == 3);
+ assert!(dns.header.ancount == 0);
+ assert!(dns.header.nscount == 0);
+ assert!(dns.header.arcount == 0);
+ assert!(dns.qd.len() == 3);
+ assert!(
+ dns.qd[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[0].type_ == DNSType::A);
+ assert!(dns.qd[0].class == DNSClass::IN);
+ assert!(
+ dns.qd[1].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[1].type_ == DNSType::A);
+ assert!(dns.qd[1].class == DNSClass::IN);
+ assert!(
+ dns.qd[2].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[2].type_ == DNSType::A);
+ assert!(dns.qd[2].class == DNSClass::IN);
+ }
+
+ #[test]
+ fn parse_rr_all() {
+ /* OK */
+ /* scapy: DNS(id=1234, qr=True, aa=True, qd=None,
+ * an=DNSRR(rrname="www.example1.com", rdata="127.0.0.1")/DNSRR(rrname="www.example2.com", rdata="127.0.0.2")/DNSRR(rrname="www.example3.com", rdata="127.0.0.3"))
+ **/
+ let payload = b"\x04\xd2\x85\x00\x00\x00\x00\x03\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03";
+ let dns = match DNSPacket::try_from(payload.to_vec()) {
+ Ok(_dns) => _dns,
+ Err(e) => panic!("error while parsing DNS packet: {}", e),
+ };
+ assert!(dns.header.id == 1234);
+ assert!(dns.header._qr == true);
+ assert!(dns.header._opcode == 0);
+ assert!(dns.header._aa == true);
+ assert!(dns.header._tc == false);
+ assert!(dns.header._rd == true);
+ assert!(dns.header._ra == false);
+ assert!(dns.header._z == 0);
+ assert!(dns.header._rcode == 0);
+ assert!(dns.header.qdcount == 0);
+ assert!(dns.header.ancount == 3);
+ assert!(dns.header.nscount == 0);
+ assert!(dns.header.arcount == 0);
+ assert!(dns.rr.len() == 3);
+ assert!(
+ dns.rr[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[0].type_ == DNSType::A);
+ assert!(dns.rr[0].class == DNSClass::IN);
+ assert!(dns.rr[0].rdata == [0x7f, 0x00, 0x00, 0x01]);
+ assert!(
+ dns.rr[1].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[1].type_ == DNSType::A);
+ assert!(dns.rr[1].class == DNSClass::IN);
+ assert!(dns.rr[1].rdata == [0x7f, 0x00, 0x00, 0x02]);
+ assert!(
+ dns.rr[2].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[2].type_ == DNSType::A);
+ assert!(dns.rr[2].class == DNSClass::IN);
+ assert!(dns.rr[2].rdata == [0x7f, 0x00, 0x00, 0x03]);
+ /* KO */
+ let payload = b"\x04\xd2\x85\x00\x00\x00\x00\x04\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03";
+ match DNSPacket::try_from(payload.to_vec()) {
+ Ok(_) => panic!("parsing should have failed"),
+ Err(_) => {}
+ }
+ let payload = b"xxx";
+ match DNSPacket::try_from(payload.to_vec()) {
+ Ok(_) => panic!("parsing should have failed"),
+ Err(_) => {}
+ }
+ }
+
+ #[test]
+ fn parse_rr_byte_by_byte() {
+ /* scapy: DNS(id=1234, qr=True, aa=True, qd=None,
+ * an=DNSRR(rrname="www.example1.com", rdata="127.0.0.1")/DNSRR(rrname="www.example2.com", rdata="127.0.0.2")/DNSRR(rrname="www.example3.com", rdata="127.0.0.3"))
+ **/
+ let payload = b"\x04\xd2\x85\x00\x00\x00\x00\x03\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03";
+ let mut dns = DNSPacket::new();
+ for b in payload {
+ assert!(dns.d.state != DNSState::End);
+ dns.parse(&b);
+ }
+ assert!(dns.d.state == DNSState::End);
+ assert!(dns.header.id == 1234);
+ assert!(dns.header._qr == true);
+ assert!(dns.header._opcode == 0);
+ assert!(dns.header._aa == true);
+ assert!(dns.header._tc == false);
+ assert!(dns.header._rd == true);
+ assert!(dns.header._ra == false);
+ assert!(dns.header._z == 0);
+ assert!(dns.header._rcode == 0);
+ assert!(dns.header.qdcount == 0);
+ assert!(dns.header.ancount == 3);
+ assert!(dns.header.nscount == 0);
+ assert!(dns.header.arcount == 0);
+ assert!(dns.rr.len() == 3);
+ assert!(
+ dns.rr[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[0].type_ == DNSType::A);
+ assert!(dns.rr[0].class == DNSClass::IN);
+ assert!(dns.rr[0].rdata == [0x7f, 0x00, 0x00, 0x01]);
+ assert!(
+ dns.rr[1].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[1].type_ == DNSType::A);
+ assert!(dns.rr[1].class == DNSClass::IN);
+ assert!(dns.rr[1].rdata == [0x7f, 0x00, 0x00, 0x02]);
+ assert!(
+ dns.rr[2].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[2].type_ == DNSType::A);
+ assert!(dns.rr[2].class == DNSClass::IN);
+ assert!(dns.rr[2].rdata == [0x7f, 0x00, 0x00, 0x03]);
+ }
+
+ #[test]
+ fn parse_qd_rr_all() {
+ /* scapy: DNS(id=1234, qr=True, aa=True,
+ * qd=DNSQR(qname="www.example1.com")/DNSQR(qname="www.example2.com")/DNSQR(qname="www.example3.com"),
+ * an=DNSRR(rrname="www.example1.com", rdata="127.0.0.1")/DNSRR(rrname="www.example2.com", rdata="127.0.0.2")/DNSRR(rrname="www.example3.com", rdata="127.0.0.3"))
+ */
+ let payload = b"\x04\xd2\x85\x00\x00\x03\x00\x03\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03";
+ let dns = match DNSPacket::try_from(payload.to_vec()) {
+ Ok(_dns) => _dns,
+ Err(e) => panic!("error while parsing DNS packet: {}", e),
+ };
+ assert!(dns.header.id == 1234);
+ assert!(dns.header._qr == true);
+ assert!(dns.header._opcode == 0);
+ assert!(dns.header._aa == true);
+ assert!(dns.header._tc == false);
+ assert!(dns.header._rd == true);
+ assert!(dns.header._ra == false);
+ assert!(dns.header._z == 0);
+ assert!(dns.header._rcode == 0);
+ assert!(dns.header.qdcount == 3);
+ assert!(dns.header.ancount == 3);
+ assert!(dns.header.nscount == 0);
+ assert!(dns.header.arcount == 0);
+ assert!(dns.qd.len() == 3);
+ assert!(
+ dns.qd[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[0].type_ == DNSType::A);
+ assert!(dns.qd[0].class == DNSClass::IN);
+ assert!(
+ dns.qd[1].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[1].type_ == DNSType::A);
+ assert!(dns.qd[1].class == DNSClass::IN);
+ assert!(
+ dns.qd[2].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[2].type_ == DNSType::A);
+ assert!(dns.qd[2].class == DNSClass::IN);
+ assert!(dns.rr.len() == 3);
+ assert!(
+ dns.rr[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[0].type_ == DNSType::A);
+ assert!(dns.rr[0].class == DNSClass::IN);
+ assert!(dns.rr[0].rdata == [0x7f, 0x00, 0x00, 0x01]);
+ assert!(
+ dns.rr[1].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[1].type_ == DNSType::A);
+ assert!(dns.rr[1].class == DNSClass::IN);
+ assert!(dns.rr[1].rdata == [0x7f, 0x00, 0x00, 0x02]);
+ assert!(
+ dns.rr[2].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[2].type_ == DNSType::A);
+ assert!(dns.rr[2].class == DNSClass::IN);
+ assert!(dns.rr[2].rdata == [0x7f, 0x00, 0x00, 0x03]);
+ }
+
+ #[test]
+ fn parse_qr_rr_byte_by_byte() {
+ /* scapy: DNS(id=1234, qr=True, aa=True,
+ * qd=DNSQR(qname="www.example1.com")/DNSQR(qname="www.example2.com")/DNSQR(qname="www.example3.com"),
+ * an=DNSRR(rrname="www.example1.com", rdata="127.0.0.1")/DNSRR(rrname="www.example2.com", rdata="127.0.0.2")/DNSRR(rrname="www.example3.com", rdata="127.0.0.3"))
+ */
+ let payload = b"\x04\xd2\x85\x00\x00\x03\x00\x03\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x02\x03www\x08example3\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x03";
+ let mut dns = DNSPacket::new();
+ for b in payload {
+ assert!(dns.d.state != DNSState::End);
+ dns.parse(&b);
+ }
+ assert!(dns.d.state == DNSState::End);
+ assert!(dns.header.id == 1234);
+ assert!(dns.header._qr == true);
+ assert!(dns.header._opcode == 0);
+ assert!(dns.header._aa == true);
+ assert!(dns.header._tc == false);
+ assert!(dns.header._rd == true);
+ assert!(dns.header._ra == false);
+ assert!(dns.header._z == 0);
+ assert!(dns.header._rcode == 0);
+ assert!(dns.header.qdcount == 3);
+ assert!(dns.header.ancount == 3);
+ assert!(dns.header.nscount == 0);
+ assert!(dns.header.arcount == 0);
+ assert!(dns.qd.len() == 3);
+ assert!(
+ dns.qd[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[0].type_ == DNSType::A);
+ assert!(dns.qd[0].class == DNSClass::IN);
+ assert!(
+ dns.qd[1].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[1].type_ == DNSType::A);
+ assert!(dns.qd[1].class == DNSClass::IN);
+ assert!(
+ dns.qd[2].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.qd[2].type_ == DNSType::A);
+ assert!(dns.qd[2].class == DNSClass::IN);
+ assert!(dns.rr.len() == 3);
+ assert!(
+ dns.rr[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x31,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[0].type_ == DNSType::A);
+ assert!(dns.rr[0].class == DNSClass::IN);
+ assert!(dns.rr[0].rdata == [0x7f, 0x00, 0x00, 0x01]);
+ assert!(
+ dns.rr[1].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x32,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[1].type_ == DNSType::A);
+ assert!(dns.rr[1].class == DNSClass::IN);
+ assert!(dns.rr[1].rdata == [0x7f, 0x00, 0x00, 0x02]);
+ assert!(
+ dns.rr[2].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x08, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x33,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(dns.rr[2].type_ == DNSType::A);
+ assert!(dns.rr[2].class == DNSClass::IN);
+ assert!(dns.rr[2].rdata == [0x7f, 0x00, 0x00, 0x03]);
+ }
+
+ #[test]
+ fn reply_in_a() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let mut client_info = ClientInfo::new();
+ /* scapy: DNS(id=0x1337,
+ * qd=DNSQR(qname="www.example.com"))
+ **/
+ let payload = b"\x137\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03www\x07example\x03com\x00\x00\x01\x00\x01";
+ let dns = DNSPacket::try_from(payload.to_vec()).unwrap();
+ for ip in [
+ Ipv4Addr::new(127, 0, 0, 1),
+ Ipv4Addr::new(0, 0, 0, 0),
+ Ipv4Addr::new(4, 3, 2, 1),
+ ] {
+ client_info.ip.dst = Some(IpAddr::V4(ip));
+ let ans = if let Some(a) = dns.repl(&masscanned, &client_info, None) {
+ DNSPacket::try_from(a).unwrap()
+ } else {
+ panic!("expected a reply, got None");
+ };
+ assert!(ans.header.id == 0x1337);
+ assert!(ans.header._qr == true);
+ assert!(ans.header._opcode == 0);
+ assert!(ans.header._aa == true);
+ assert!(ans.header._tc == false);
+ assert!(ans.header._rd == dns.header._rd);
+ assert!(ans.header._ra == false);
+ assert!(ans.header._z == 0);
+ assert!(ans.header._rcode == 0);
+ assert!(ans.header.qdcount == 1);
+ assert!(ans.header.ancount == 1);
+ assert!(ans.header.nscount == 0);
+ assert!(ans.header.arcount == 0);
+ assert!(ans.qd.len() == 1);
+ assert!(
+ ans.qd[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(ans.qd[0].type_ == DNSType::A);
+ assert!(ans.qd[0].class == DNSClass::IN);
+ assert!(ans.rr.len() == 1);
+ assert!(
+ ans.rr[0].name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
+ 0x03, 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(ans.rr[0].type_ == DNSType::A);
+ assert!(ans.rr[0].class == DNSClass::IN);
+ assert!(ans.rr[0].rdata == ip.octets());
+ }
+ }
+}
diff --git a/src/proto/dns/query.rs b/src/proto/dns/query.rs
new file mode 100644
index 0000000..95fb205
--- /dev/null
+++ b/src/proto/dns/query.rs
@@ -0,0 +1,337 @@
+// This file is part of masscanned.
+// Copyright 2022 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use super::cst::{DNSClass, DNSType};
+use super::rr::DNSRR;
+
+use std::convert::TryFrom;
+use std::net::IpAddr;
+
+use crate::proto::dissector::{MPacket, PacketDissector};
+use crate::proto::ClientInfo;
+use crate::proto::TCPControlBlock;
+use crate::Masscanned;
+
+#[derive(PartialEq)]
+pub enum DNSQueryState {
+ Name,
+ Type,
+ Class,
+ End,
+}
+
+pub struct DNSQuery {
+ pub d: PacketDissector,
+ /* RFC 1035 - Section 4.1.2 */
+ pub name: Vec,
+ _u_type: u16,
+ pub type_: DNSType,
+ _u_class: u16,
+ pub class: DNSClass,
+}
+
+impl TryFrom> for DNSQuery {
+ type Error = &'static str;
+
+ fn try_from(item: Vec) -> Result {
+ let mut query = DNSQuery::new();
+ for b in item {
+ query.parse(&b);
+ }
+ if query.d.state == DNSQueryState::End {
+ Ok(query)
+ } else {
+ Err("packet is incomplete")
+ }
+ }
+}
+
+impl From<&DNSQuery> for Vec {
+ fn from(item: &DNSQuery) -> Self {
+ let mut v = Vec::new();
+ /* name */
+ v.extend(&item.name);
+ /* type */
+ v.push(((u16::from(item.type_)) >> 8) as u8);
+ v.push(((u16::from(item.type_)) & 0xFF) as u8);
+ /* class */
+ v.push(((u16::from(item.class)) >> 8) as u8);
+ v.push(((u16::from(item.class)) & 0xFF) as u8);
+ /* return */
+ v
+ }
+}
+
+impl MPacket for DNSQuery {
+ fn new() -> Self {
+ DNSQuery {
+ d: PacketDissector::new(DNSQueryState::Name),
+ name: Vec::new(),
+ _u_type: 0,
+ type_: DNSType::NONE,
+ _u_class: 0,
+ class: DNSClass::NONE,
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ DNSQueryState::Name => {
+ self.name.push(*byte);
+ if *byte == 0 {
+ self.d.next_state(DNSQueryState::Type);
+ }
+ }
+ DNSQueryState::Type => {
+ self._u_type = self.d.read_u16(byte, self._u_type, DNSQueryState::Class);
+ }
+ DNSQueryState::Class => {
+ self._u_class = self.d.read_u16(byte, self._u_class, DNSQueryState::End);
+ }
+ DNSQueryState::End => {}
+ }
+ /* we need this to be executed at the same call
+ * the state changes to End, hence it is not in the
+ * match structure
+ **/
+ if self.d.state == DNSQueryState::End {
+ self.type_ = DNSType::from(self._u_type);
+ self.class = DNSClass::from(self._u_class);
+ }
+ }
+
+ fn repl(
+ &self,
+ _masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ match self.class {
+ DNSClass::IN => {
+ match self.type_ {
+ DNSType::A => {
+ let mut rr = DNSRR::new();
+ /* copy request */
+ for b in &self.name {
+ rr.name.push(*b);
+ }
+ rr.type_ = DNSType::A;
+ rr.class = DNSClass::IN;
+ rr.ttl = 43200;
+ rr.rdata = match client_info.ip.dst {
+ Some(IpAddr::V4(ip)) => ip.octets().to_vec(),
+ Some(IpAddr::V6(_)) => Vec::new(),
+ None => Vec::new(),
+ };
+ rr.rdlen = rr.rdata.len() as u16;
+ Some(Vec::::from(&rr))
+ }
+ _ => None,
+ }
+ }
+ _ => None,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use pnet::util::MacAddr;
+ use std::net::{IpAddr, Ipv4Addr};
+ use std::str::FromStr;
+ use strum::IntoEnumIterator;
+
+ use crate::client::ClientInfoSrcDst;
+ use crate::logger::MetaLogger;
+
+ #[test]
+ fn parse_in_a_all() {
+ /* A */
+ let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01";
+ let qr = match DNSQuery::try_from(payload.to_vec()) {
+ Ok(_qr) => _qr,
+ Err(e) => panic!("error while parsing DNS query: {}", e),
+ };
+ assert!(
+ qr.name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03,
+ 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(qr.type_ == DNSType::A);
+ assert!(qr.class == DNSClass::IN);
+ assert!(Vec::::from(&qr) == payload.to_vec());
+ /* TXT */
+ let payload = b"\x07version\x04bind\x00\x00\x10\x00\x03";
+ let qr = match DNSQuery::try_from(payload.to_vec()) {
+ Ok(_qr) => _qr,
+ Err(e) => panic!("error while parsing DNS query: {}", e),
+ };
+ assert!(qr.type_ == DNSType::TXT);
+ assert!(qr.class == DNSClass::CH);
+ assert!(Vec::::from(&qr) == payload.to_vec());
+ /* KO */
+ let payload = b"xxx";
+ match DNSQuery::try_from(payload.to_vec()) {
+ Ok(_) => panic!("parsing should have failed"),
+ Err(_) => {}
+ }
+ }
+
+ #[test]
+ fn parse_in_a_byte_by_byte() {
+ /* A */
+ let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01";
+ let mut qr = DNSQuery::new();
+ for b in payload {
+ qr.parse(b);
+ }
+ assert!(qr.d.state == DNSQueryState::End);
+ assert!(
+ qr.name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03,
+ 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(qr.type_ == DNSType::A);
+ assert!(qr.class == DNSClass::IN);
+ assert!(Vec::::from(&qr) == payload.to_vec());
+ /* TXT */
+ let payload = b"\x07version\x04bind\x00\x00\x10\x00\x03";
+ let mut qr = DNSQuery::new();
+ for b in payload {
+ qr.parse(b);
+ }
+ assert!(qr.d.state == DNSQueryState::End);
+ assert!(qr.type_ == DNSType::TXT);
+ assert!(qr.class == DNSClass::CH);
+ assert!(Vec::::from(&qr) == payload.to_vec());
+ /* KO */
+ let payload = b"xxx";
+ let mut qr = DNSQuery::new();
+ for b in payload {
+ qr.parse(b);
+ }
+ assert!(qr.d.state != DNSQueryState::End);
+ }
+
+ #[test]
+ fn reply_in_a() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let ip_src = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
+ let ip_dst = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 2));
+ let client_info = ClientInfo {
+ mac: ClientInfoSrcDst {
+ src: None,
+ dst: None,
+ },
+ ip: ClientInfoSrcDst {
+ src: Some(ip_src),
+ dst: Some(ip_dst),
+ },
+ transport: None,
+ port: ClientInfoSrcDst {
+ src: None,
+ dst: None,
+ },
+ cookie: None,
+ };
+ /* TXT */
+ let payload = b"\x07version\x04bind\x00\x00\x10\x00\x03";
+ let mut qr = DNSQuery::new();
+ for b in payload {
+ qr.parse(b);
+ }
+ assert!(qr.type_ == DNSType::TXT);
+ assert!(qr.class == DNSClass::CH);
+ /* A */
+ let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01";
+ let mut qr = DNSQuery::new();
+ for b in payload {
+ qr.parse(b);
+ }
+ assert!(
+ qr.name
+ == [
+ 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03,
+ 0x63, 0x6f, 0x6d, 0x00
+ ]
+ );
+ assert!(qr.type_ == DNSType::A);
+ assert!(qr.class == DNSClass::IN);
+ let rr_raw = match qr.repl(&masscanned, &client_info, None) {
+ None => {
+ panic!()
+ }
+ Some(r) => r,
+ };
+ let mut rr = DNSRR::new();
+ for b in rr_raw {
+ rr.parse(&b);
+ }
+ assert!(rr.name == qr.name);
+ assert!(rr.type_ == DNSType::A);
+ assert!(rr.class == DNSClass::IN);
+ assert!(rr.ttl == 43200);
+ assert!(rr.rdata == [127, 0, 0, 2]);
+ }
+
+ #[test]
+ fn repl() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let client_info = ClientInfo::new();
+ /* exhaustive tests */
+ let supported: Vec<(DNSClass, DNSType)> = vec![(DNSClass::IN, DNSType::A)];
+ let mut qd = DNSQuery::new();
+ qd.name = vec![
+ 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63,
+ 0x6f, 0x6d, 0x00,
+ ];
+ for c in DNSClass::iter() {
+ qd.class = c;
+ for t in DNSType::iter() {
+ qd.type_ = t;
+ if supported.contains(&(c, t)) {
+ if qd.repl(&masscanned, &client_info, None) == None {
+ panic!("expected reply, got None");
+ }
+ } else {
+ if qd.repl(&masscanned, &client_info, None) != None {
+ panic!("expected no reply, got one for {:?}, {:?}", c, t);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/proto/dns/rr.rs b/src/proto/dns/rr.rs
new file mode 100644
index 0000000..db188bb
--- /dev/null
+++ b/src/proto/dns/rr.rs
@@ -0,0 +1,251 @@
+// This file is part of masscanned.
+// Copyright 2022 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use super::cst::{DNSClass, DNSType};
+
+use std::convert::TryFrom;
+
+use crate::proto::dissector::{MPacket, PacketDissector};
+use crate::proto::ClientInfo;
+use crate::proto::TCPControlBlock;
+use crate::Masscanned;
+
+#[derive(PartialEq, Debug)]
+pub enum DNSRRState {
+ Name,
+ Type,
+ Class,
+ TTL,
+ RDLength,
+ RData,
+ End,
+}
+
+pub struct DNSRR {
+ pub d: PacketDissector,
+ /* RFC 1035 - Section 3.2.1 */
+ pub name: Vec,
+ _u_type: u16,
+ pub type_: DNSType,
+ _u_class: u16,
+ pub class: DNSClass,
+ pub ttl: u32,
+ pub rdlen: u16,
+ pub rdata: Vec,
+}
+
+impl From<&DNSRR> for Vec {
+ fn from(item: &DNSRR) -> Self {
+ /* CAUTION: for the rdlen field:
+ * - if item.rdlen is not 0, its value is packed
+ * - if item.rdlen = 0, then the length of item.rdata is used instead
+ */
+ let mut v = Vec::new();
+ /* name */
+ for b in &item.name {
+ v.push(b.clone());
+ }
+ /* type */
+ let type_: u16 = item.type_.into();
+ v.push((type_ >> 8) as u8);
+ v.push((type_ & 0xFF) as u8);
+ /* class */
+ let class: u16 = item.class.into();
+ v.push((class >> 8) as u8);
+ v.push((class & 0xFF) as u8);
+ /* ttl */
+ v.push((item.ttl >> 24) as u8);
+ v.push((item.ttl >> 16) as u8);
+ v.push((item.ttl >> 8) as u8);
+ v.push((item.ttl & 0xFF) as u8);
+ /* rdlen */
+ let rdlen = if item.rdlen == 0 {
+ item.rdata.len() as u16
+ } else {
+ item.rdlen
+ };
+ v.push((rdlen >> 8) as u8);
+ v.push((rdlen & 0xFF) as u8);
+ /* rdata */
+ for b in &item.rdata {
+ v.push(b.clone());
+ }
+ v
+ }
+}
+
+impl TryFrom> for DNSRR {
+ type Error = &'static str;
+
+ fn try_from(item: Vec) -> Result {
+ let mut rr = DNSRR::new();
+ for b in item {
+ rr.parse(&b);
+ }
+ if rr.d.state == DNSRRState::End {
+ Ok(rr)
+ } else {
+ Err("packet is incomplete")
+ }
+ }
+}
+
+impl MPacket for DNSRR {
+ fn new() -> Self {
+ DNSRR {
+ d: PacketDissector::new(DNSRRState::Name),
+ name: Vec::new(),
+ _u_type: 0,
+ type_: DNSType::NONE,
+ _u_class: 0,
+ class: DNSClass::NONE,
+ rdlen: 0,
+ ttl: 0,
+ rdata: Vec::new(),
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ DNSRRState::Name => {
+ self.name.push(*byte);
+ if *byte == 0 {
+ self.d.next_state(DNSRRState::Type);
+ }
+ }
+ DNSRRState::Type => {
+ self._u_type = self.d.read_u16(byte, self._u_type, DNSRRState::Class);
+ }
+ DNSRRState::Class => {
+ self._u_class = self.d.read_u16(byte, self._u_class, DNSRRState::TTL);
+ }
+ DNSRRState::TTL => {
+ self.ttl = self.d.read_u32(byte, self.ttl, DNSRRState::RDLength);
+ }
+ DNSRRState::RDLength => {
+ self.rdlen = self.d.read_u16(byte, self.rdlen, DNSRRState::RData);
+ /* when read the rdlen, check if len is 0 */
+ if self.d.state == DNSRRState::RData && self.rdlen == 0 {
+ self.d.state = DNSRRState::End;
+ }
+ }
+ DNSRRState::RData => {
+ self.rdata.push(*byte);
+ if self.rdata.len() == self.rdlen as usize {
+ self.d.next_state(DNSRRState::End);
+ }
+ }
+ DNSRRState::End => {}
+ }
+ /* we need this to be executed at the same call
+ * the state changes to End, hence it is not in the
+ * match structure
+ **/
+ if self.d.state == DNSRRState::End {
+ self.type_ = DNSType::from(self._u_type);
+ self.class = DNSClass::from(self._u_class);
+ }
+ }
+
+ fn repl(
+ &self,
+ _masscanned: &Masscanned,
+ _client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn build() {
+ let mut rr = DNSRR::new();
+ rr.name = b"\x03www\x07example\x03com\x00".to_vec();
+ rr.class = DNSClass::IN;
+ rr.type_ = DNSType::A;
+ rr.ttl = 1234;
+ rr.rdlen = 4;
+ rr.rdata = b"\x7f\x00\x00\x01".to_vec();
+ assert!(Vec::::from(&rr) == b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x04\xd2\x00\x04\x7f\x00\x00\x01");
+ }
+
+ #[test]
+ fn parse_all() {
+ /*
+ * raw(DNSRR(rrname="www.example.com", rdata="127.0.0.1"))
+ */
+ let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01";
+ let rr = match DNSRR::try_from(payload.to_vec()) {
+ Ok(r) => r,
+ Err(e) => panic!("error while parsing DNS RR: {}", e),
+ };
+ assert!(rr.name == b"\x03www\x07example\x03com\x00");
+ assert!(rr.class == DNSClass::IN);
+ assert!(rr.type_ == DNSType::A);
+ assert!(rr.rdata == b"\x7f\x00\x00\x01");
+ assert!(Vec::::from(&rr) == payload.to_vec());
+ /*
+ * empty data
+ */
+ let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00";
+ let rr = match DNSRR::try_from(payload.to_vec()) {
+ Ok(r) => r,
+ Err(e) => panic!("error while parsing DNS RR: {}", e),
+ };
+ assert!(rr.name == b"\x03www\x07example\x03com\x00");
+ assert!(rr.class == DNSClass::IN);
+ assert!(rr.type_ == DNSType::A);
+ assert!(rr.rdata == b"");
+ assert!(Vec::::from(&rr) == payload.to_vec());
+ }
+
+ #[test]
+ fn parse_byte_by_byte() {
+ /*
+ * raw(DNSRR(rrname="www.example.com", rdata="127.0.0.1"))
+ */
+ let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x7f\x00\x00\x01";
+ let mut rr = DNSRR::new();
+ for b in payload {
+ assert!(rr.d.state != DNSRRState::End);
+ rr.parse(b);
+ }
+ assert!(rr.d.state == DNSRRState::End);
+ assert!(rr.name == b"\x03www\x07example\x03com\x00");
+ assert!(rr.class == DNSClass::IN);
+ assert!(rr.type_ == DNSType::A);
+ assert!(rr.rdata == b"\x7f\x00\x00\x01");
+ assert!(Vec::::from(&rr) == payload.to_vec());
+ /*
+ * empty data
+ */
+ let payload = b"\x03www\x07example\x03com\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00";
+ let mut rr = DNSRR::new();
+ for b in payload {
+ assert!(rr.d.state != DNSRRState::End);
+ rr.parse(b);
+ }
+ assert!(rr.name == b"\x03www\x07example\x03com\x00");
+ assert!(rr.class == DNSClass::IN);
+ assert!(rr.type_ == DNSType::A);
+ assert!(rr.rdata == b"");
+ assert!(Vec::::from(&rr) == payload.to_vec());
+ }
+}
diff --git a/src/proto/ghost.rs b/src/proto/ghost.rs
new file mode 100644
index 0000000..73d482e
--- /dev/null
+++ b/src/proto/ghost.rs
@@ -0,0 +1,59 @@
+// This file is part of masscanned.
+// Copyright 2021 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use log::*;
+use std::io::Write;
+
+use flate2::write::ZlibEncoder;
+use flate2::Compression;
+
+use crate::client::ClientInfo;
+use crate::proto::TCPControlBlock;
+use crate::Masscanned;
+
+pub const GHOST_PATTERN_SIGNATURE: &[u8; 5] = b"Gh0st";
+
+pub fn repl<'a>(
+ _data: &'a [u8],
+ _masscanned: &Masscanned,
+ _client_info: &mut ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+) -> Option> {
+ debug!("receiving Gh0st data, sending one null byte payload");
+ // Packet structure:
+ // GHOST_PATTERN_SIGNATURE + [ packet size ] + [ uncompressed payload size ] + payload
+ let mut result = GHOST_PATTERN_SIGNATURE.to_vec();
+ let uncompressed_data = b"\x00";
+ let mut compressed_data = ZlibEncoder::new(Vec::new(), Compression::default());
+ compressed_data
+ .write_all(uncompressed_data)
+ .expect("Ghost: cannot decompress payload");
+ let mut compressed_data = compressed_data
+ .finish()
+ .expect("Ghost: cannot decompress payload");
+ let mut packet_len = compressed_data.len() + GHOST_PATTERN_SIGNATURE.len() + 4 * 2;
+ for _ in 0..4 {
+ result.push((packet_len % 256) as u8);
+ packet_len /= 256;
+ }
+ let mut uncompressed_len = uncompressed_data.len();
+ for _ in 0..4 {
+ result.push((uncompressed_len % 256) as u8);
+ uncompressed_len /= 256;
+ }
+ result.append(&mut compressed_data);
+ Some(result)
+}
diff --git a/src/proto/http.rs b/src/proto/http.rs
index 8a3da01..5f28e8a 100644
--- a/src/proto/http.rs
+++ b/src/proto/http.rs
@@ -21,6 +21,7 @@ use lazy_static::lazy_static;
use std::str;
use crate::client::ClientInfo;
+use crate::proto::{ProtocolState as GenericProtocolState, TCPControlBlock};
use crate::smack::{
Smack, SmackFlags, BASE_STATE, NO_MATCH, SMACK_CASE_INSENSITIVE, UNANCHORED_STATE,
};
@@ -62,7 +63,7 @@ const HTTP_STATE_CONTENT: usize = 64;
const HTTP_STATE_FAIL: usize = 0xFFFF;
-struct ProtocolState {
+pub struct ProtocolState {
state: usize,
state_bis: usize,
smack_state: usize,
@@ -223,15 +224,39 @@ pub fn repl<'a>(
data: &'a [u8],
_masscanned: &Masscanned,
_client_info: &ClientInfo,
+ tcb: Option<&mut TCPControlBlock>,
) -> Option> {
debug!("receiving HTTP data");
- let mut pstate = ProtocolState::new();
+ let mut state = ProtocolState::new();
+ let mut pstate = {
+ if let Some(t) = tcb {
+ match t.proto_state {
+ None => t.proto_state = Some(GenericProtocolState::HTTP(ProtocolState::new())),
+ Some(GenericProtocolState::HTTP(_)) => {}
+ _ => {
+ panic!()
+ }
+ };
+ if let Some(GenericProtocolState::HTTP(p)) = &mut t.proto_state {
+ p
+ } else {
+ panic!();
+ }
+ } else {
+ &mut state
+ }
+ };
http_parse(&mut pstate, data);
if pstate.state == HTTP_STATE_FAIL {
debug!("data in not correctly formatted - not responding");
debug!("pstate: {}", pstate.state);
return None;
}
+ /* if not in CONTENT state, not responding yet (it means the client
+ * has not finished sending headers yet) */
+ if pstate.state != HTTP_STATE_CONTENT {
+ return None;
+ }
let content = "\
401 Authorization Required
@@ -267,122 +292,127 @@ WWW-Authenticate: Basic realm=\"Access to admin page\"
Some(repl_data)
}
-#[test]
-fn test_http_verb() {
- /* all at once */
- for verb in HTTP_VERBS.iter() {
- let mut pstate = ProtocolState::new();
- assert!(pstate.state == HTTP_STATE_START);
- assert!(pstate.smack_state == BASE_STATE);
- assert!(pstate.smack_id == NO_MATCH);
- http_parse(&mut pstate, &verb.as_bytes());
- assert!(pstate.state == HTTP_STATE_SPACE);
- assert!(pstate.smack_id == (HttpField::Verb as usize));
- assert!(pstate.http_verb == verb.as_bytes());
- }
- /* byte by byte */
- for verb in HTTP_VERBS.iter() {
- let mut pstate = ProtocolState::new();
- assert!(pstate.state == HTTP_STATE_START);
- assert!(pstate.smack_state == BASE_STATE);
- assert!(pstate.smack_id == NO_MATCH);
- for i in 0..verb.len() {
- if i > 0 {
- assert!(pstate.state == HTTP_STATE_VERB);
- assert!(pstate.smack_id == NO_MATCH);
- }
- http_parse(&mut pstate, &verb.as_bytes()[i..i + 1]);
- }
- assert!(pstate.state == HTTP_STATE_SPACE);
- assert!(pstate.smack_id == (HttpField::Verb as usize));
- assert!(pstate.http_verb == verb.as_bytes());
- }
- /* KO test: XXX */
- let mut pstate = ProtocolState::new();
- assert!(pstate.state == HTTP_STATE_START);
- assert!(pstate.smack_state == BASE_STATE);
- assert!(pstate.smack_id == NO_MATCH);
- http_parse(&mut pstate, "XXX".as_bytes());
- assert!(pstate.state == HTTP_STATE_FAIL);
- assert!(pstate.smack_state == UNANCHORED_STATE);
- assert!(pstate.smack_id == NO_MATCH);
- /* KO test: XGET */
- let mut pstate = ProtocolState::new();
- assert!(pstate.state == HTTP_STATE_START);
- assert!(pstate.smack_state == BASE_STATE);
- assert!(pstate.smack_id == NO_MATCH);
- http_parse(&mut pstate, "XGET".as_bytes());
- assert!(pstate.state == HTTP_STATE_FAIL);
- assert!(pstate.smack_state == UNANCHORED_STATE);
- assert!(pstate.smack_id == NO_MATCH);
- /* KO test: GEX */
- let mut pstate = ProtocolState::new();
- assert!(pstate.state == HTTP_STATE_START);
- assert!(pstate.smack_state == BASE_STATE);
- assert!(pstate.smack_id == NO_MATCH);
- http_parse(&mut pstate, "GEX".as_bytes());
- assert!(pstate.state == HTTP_STATE_FAIL);
- assert!(pstate.smack_state == UNANCHORED_STATE);
- assert!(pstate.smack_id == NO_MATCH);
- /* KO test: GE T */
- let mut pstate = ProtocolState::new();
- assert!(pstate.state == HTTP_STATE_START);
- assert!(pstate.smack_state == BASE_STATE);
- assert!(pstate.smack_id == NO_MATCH);
- http_parse(&mut pstate, "GE T".as_bytes());
- assert!(pstate.state == HTTP_STATE_FAIL);
- assert!(pstate.smack_state == UNANCHORED_STATE);
- assert!(pstate.smack_id == NO_MATCH);
-}
+#[cfg(test)]
+mod tests {
+ use super::*;
-#[test]
-fn test_http_request_line() {
- let mut pstate = ProtocolState::new();
- let data = "GET /index.php HTTP/1.1\r\n".as_bytes();
- for i in 0..data.len() {
- http_parse(&mut pstate, &data[i..i + 1]);
- if i < 2 {
- assert!(pstate.state == HTTP_STATE_VERB);
- } else if i == 2 {
+ #[test]
+ fn test_http_verb() {
+ /* all at once */
+ for verb in HTTP_VERBS.iter() {
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == HTTP_STATE_START);
+ assert!(pstate.smack_state == BASE_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ http_parse(&mut pstate, &verb.as_bytes());
assert!(pstate.state == HTTP_STATE_SPACE);
- } else if 3 <= i && i <= 13 {
- assert!(pstate.state == HTTP_STATE_URI);
- } else if 14 <= i && i <= 19 {
- assert!(pstate.state == HTTP_STATE_H + (i - 14));
- } else if i == 20 {
- assert!(pstate.state == HTTP_STATE_VERSION_MAJ);
- } else if 21 <= i && i <= 23 {
- assert!(pstate.state == HTTP_STATE_VERSION_MIN);
- } else if i == 24 {
- assert!(pstate.state == HTTP_STATE_FIELD_START);
+ assert!(pstate.smack_id == (HttpField::Verb as usize));
+ assert!(pstate.http_verb == verb.as_bytes());
+ }
+ /* byte by byte */
+ for verb in HTTP_VERBS.iter() {
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == HTTP_STATE_START);
+ assert!(pstate.smack_state == BASE_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ for i in 0..verb.len() {
+ if i > 0 {
+ assert!(pstate.state == HTTP_STATE_VERB);
+ assert!(pstate.smack_id == NO_MATCH);
+ }
+ http_parse(&mut pstate, &verb.as_bytes()[i..i + 1]);
+ }
+ assert!(pstate.state == HTTP_STATE_SPACE);
+ assert!(pstate.smack_id == (HttpField::Verb as usize));
+ assert!(pstate.http_verb == verb.as_bytes());
+ }
+ /* KO test: XXX */
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == HTTP_STATE_START);
+ assert!(pstate.smack_state == BASE_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ http_parse(&mut pstate, "XXX".as_bytes());
+ assert!(pstate.state == HTTP_STATE_FAIL);
+ assert!(pstate.smack_state == UNANCHORED_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ /* KO test: XGET */
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == HTTP_STATE_START);
+ assert!(pstate.smack_state == BASE_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ http_parse(&mut pstate, "XGET".as_bytes());
+ assert!(pstate.state == HTTP_STATE_FAIL);
+ assert!(pstate.smack_state == UNANCHORED_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ /* KO test: GEX */
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == HTTP_STATE_START);
+ assert!(pstate.smack_state == BASE_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ http_parse(&mut pstate, "GEX".as_bytes());
+ assert!(pstate.state == HTTP_STATE_FAIL);
+ assert!(pstate.smack_state == UNANCHORED_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ /* KO test: GE T */
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == HTTP_STATE_START);
+ assert!(pstate.smack_state == BASE_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ http_parse(&mut pstate, "GE T".as_bytes());
+ assert!(pstate.state == HTTP_STATE_FAIL);
+ assert!(pstate.smack_state == UNANCHORED_STATE);
+ assert!(pstate.smack_id == NO_MATCH);
+ }
+
+ #[test]
+ fn test_http_request_line() {
+ let mut pstate = ProtocolState::new();
+ let data = "GET /index.php HTTP/1.1\r\n".as_bytes();
+ for i in 0..data.len() {
+ http_parse(&mut pstate, &data[i..i + 1]);
+ if i < 2 {
+ assert!(pstate.state == HTTP_STATE_VERB);
+ } else if i == 2 {
+ assert!(pstate.state == HTTP_STATE_SPACE);
+ } else if 3 <= i && i <= 13 {
+ assert!(pstate.state == HTTP_STATE_URI);
+ } else if 14 <= i && i <= 19 {
+ assert!(pstate.state == HTTP_STATE_H + (i - 14));
+ } else if i == 20 {
+ assert!(pstate.state == HTTP_STATE_VERSION_MAJ);
+ } else if 21 <= i && i <= 23 {
+ assert!(pstate.state == HTTP_STATE_VERSION_MIN);
+ } else if i == 24 {
+ assert!(pstate.state == HTTP_STATE_FIELD_START);
+ }
}
}
-}
-#[test]
-fn test_http_request_field() {
- let mut pstate = ProtocolState::new();
- let req = "POST /index.php HTTP/2.0\r\n".as_bytes();
- http_parse(&mut pstate, req);
- assert!(pstate.state == HTTP_STATE_FIELD_START);
- let field = b"Content-Length";
- http_parse(&mut pstate, field);
- assert!(pstate.state == HTTP_STATE_FIELD_NAME);
- let dot = b": ";
- http_parse(&mut pstate, dot);
- assert!(pstate.state == HTTP_STATE_FIELD_VALUE);
- let value = b": 0\r\n";
- http_parse(&mut pstate, value);
- assert!(pstate.state == HTTP_STATE_FIELD_START);
-}
+ #[test]
+ fn test_http_request_field() {
+ let mut pstate = ProtocolState::new();
+ let req = "POST /index.php HTTP/2.0\r\n".as_bytes();
+ http_parse(&mut pstate, req);
+ assert!(pstate.state == HTTP_STATE_FIELD_START);
+ let field = b"Content-Length";
+ http_parse(&mut pstate, field);
+ assert!(pstate.state == HTTP_STATE_FIELD_NAME);
+ let dot = b": ";
+ http_parse(&mut pstate, dot);
+ assert!(pstate.state == HTTP_STATE_FIELD_VALUE);
+ let value = b": 0\r\n";
+ http_parse(&mut pstate, value);
+ assert!(pstate.state == HTTP_STATE_FIELD_START);
+ }
-#[test]
-fn test_http_request_no_field() {
- let mut pstate = ProtocolState::new();
- let req = "POST /index.php HTTP/2.0\r\n".as_bytes();
- http_parse(&mut pstate, req);
- assert!(pstate.state == HTTP_STATE_FIELD_START);
- let crlf = "\r\n".as_bytes();
- http_parse(&mut pstate, crlf);
- assert!(pstate.state == HTTP_STATE_CONTENT);
+ #[test]
+ fn test_http_request_no_field() {
+ let mut pstate = ProtocolState::new();
+ let req = "POST /index.php HTTP/2.0\r\n".as_bytes();
+ http_parse(&mut pstate, req);
+ assert!(pstate.state == HTTP_STATE_FIELD_START);
+ let crlf = "\r\n".as_bytes();
+ http_parse(&mut pstate, crlf);
+ assert!(pstate.state == HTTP_STATE_CONTENT);
+ }
}
diff --git a/src/proto/mod.rs b/src/proto/mod.rs
index 5094c75..87cfe3c 100644
--- a/src/proto/mod.rs
+++ b/src/proto/mod.rs
@@ -17,13 +17,15 @@
use lazy_static::lazy_static;
use log::*;
use pnet::packet::ip::IpNextHeaderProtocols;
-use std::collections::HashMap;
-use std::sync::Mutex;
+use std::convert::TryFrom;
use crate::client::ClientInfo;
use crate::smack::{Smack, SmackFlags, BASE_STATE, NO_MATCH, SMACK_CASE_SENSITIVE};
use crate::Masscanned;
+mod dns;
+use dns::DNSPacket;
+
mod http;
use http::HTTP_VERBS;
@@ -31,19 +33,38 @@ mod stun;
use stun::{STUN_PATTERN_CHANGE_REQUEST, STUN_PATTERN_EMPTY, STUN_PATTERN_MAGIC};
mod ssh;
-use ssh::SSH_PATTERN_CLIENT_PROTOCOL;
+use ssh::{SSH_PATTERN_CLIENT_PROTOCOL_1, SSH_PATTERN_CLIENT_PROTOCOL_2};
+mod ghost;
+use ghost::GHOST_PATTERN_SIGNATURE;
+
+mod rpc;
+use rpc::{RPC_CALL_TCP, RPC_CALL_UDP};
+
+mod smb;
+use smb::{SMB1_PATTERN_MAGIC, SMB2_PATTERN_MAGIC};
+
+mod dissector;
+use dissector::MPacket;
+
+// mod dissector;
+// pub use dissector::PacketDissector;
+//
+mod tcb;
+pub use tcb::{add_tcb, get_tcb, is_tcb_set, ProtocolState, TCPControlBlock};
+
+const PROTO_NONE: usize = 0;
const PROTO_HTTP: usize = 1;
const PROTO_STUN: usize = 2;
const PROTO_SSH: usize = 3;
-
-struct TCPControlBlock {
- proto_state: usize,
-}
+const PROTO_GHOST: usize = 4;
+const PROTO_RPC_TCP: usize = 5;
+const PROTO_RPC_UDP: usize = 6;
+const PROTO_SMB1: usize = 7;
+const PROTO_SMB2: usize = 8;
lazy_static! {
static ref PROTO_SMACK: Smack = proto_init();
- static ref CONTABLE: Mutex> = Mutex::new(HashMap::new());
}
fn proto_init() -> Smack {
@@ -72,10 +93,40 @@ fn proto_init() -> Smack {
SmackFlags::ANCHOR_BEGIN | SmackFlags::ANCHOR_END | SmackFlags::WILDCARDS,
);
smack.add_pattern(
- SSH_PATTERN_CLIENT_PROTOCOL,
+ SSH_PATTERN_CLIENT_PROTOCOL_2,
PROTO_SSH,
SmackFlags::ANCHOR_BEGIN,
);
+ smack.add_pattern(
+ SSH_PATTERN_CLIENT_PROTOCOL_1,
+ PROTO_SSH,
+ SmackFlags::ANCHOR_BEGIN,
+ );
+ smack.add_pattern(
+ GHOST_PATTERN_SIGNATURE,
+ PROTO_GHOST,
+ SmackFlags::ANCHOR_BEGIN,
+ );
+ smack.add_pattern(
+ RPC_CALL_TCP,
+ PROTO_RPC_TCP,
+ SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS,
+ );
+ smack.add_pattern(
+ RPC_CALL_UDP,
+ PROTO_RPC_UDP,
+ SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS,
+ );
+ smack.add_pattern(
+ SMB1_PATTERN_MAGIC,
+ PROTO_SMB1,
+ SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS,
+ );
+ smack.add_pattern(
+ SMB2_PATTERN_MAGIC,
+ PROTO_SMB2,
+ SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS,
+ );
smack.compile();
smack
}
@@ -84,50 +135,59 @@ pub fn repl<'a>(
data: &'a [u8],
masscanned: &Masscanned,
mut client_info: &mut ClientInfo,
+ mut tcb: Option<&mut TCPControlBlock>,
) -> Option> {
debug!("packet payload: {:?}", data);
let mut id;
if client_info.transport == Some(IpNextHeaderProtocols::Tcp) && client_info.cookie == None {
error!("Unexpected empty cookie");
return None;
- } else if client_info.cookie != None {
+ } else if let Some(t) = &mut tcb {
/* proto over TCP */
- let cookie = client_info.cookie.unwrap();
- let mut ct = CONTABLE.lock().unwrap();
- if !ct.contains_key(&cookie) {
- ct.insert(
- cookie,
- TCPControlBlock {
- proto_state: BASE_STATE,
- },
- );
- }
let mut i = 0;
- let mut tcb = ct.get_mut(&cookie).unwrap();
- let mut state = tcb.proto_state;
- id = PROTO_SMACK.search_next(&mut state, &data.to_vec(), &mut i);
- tcb.proto_state = state;
+ if t.proto_id == PROTO_NONE {
+ let mut state = t.smack_state;
+ t.proto_id = PROTO_SMACK.search_next(&mut state, data, &mut i);
+ t.smack_state = state;
+ }
+ id = t.proto_id;
} else {
/* proto over else (e.g., UDP) */
let mut i = 0;
let mut state = BASE_STATE;
- id = PROTO_SMACK.search_next(&mut state, &data.to_vec(), &mut i);
+ id = PROTO_SMACK.search_next(&mut state, data, &mut i);
/* because we are not over TCP, we can afford to assume end of pattern */
if id == NO_MATCH {
id = PROTO_SMACK.search_next_end(&mut state);
}
+ /* still no match: let us try to parse packet with protocoles
+ * that are not matched with a regex */
+ if id == NO_MATCH {
+ /* try to parse data as a DNS packet */
+ if let Ok(dns) = DNSPacket::try_from(data.to_vec()) {
+ if let Some(r) = dns.repl(&masscanned, &client_info, None) {
+ return Some(r);
+ }
+ }
+ }
}
/* proto over else (e.g., UDP) */
- if id == PROTO_HTTP {
- return http::repl(data, masscanned, client_info);
- } else if id == PROTO_STUN {
- return stun::repl(data, masscanned, &mut client_info);
- } else if id == PROTO_SSH {
- return ssh::repl(data, masscanned, &mut client_info);
- } else {
- debug!("id: {}", id);
+ match id {
+ PROTO_HTTP => http::repl(data, masscanned, client_info, tcb),
+ PROTO_STUN => stun::repl(data, masscanned, &mut client_info, tcb),
+ PROTO_SSH => ssh::repl(data, masscanned, &mut client_info, tcb),
+ PROTO_GHOST => ghost::repl(data, masscanned, &mut client_info, tcb),
+ PROTO_RPC_TCP => rpc::repl_tcp(data, masscanned, &mut client_info, tcb),
+ PROTO_RPC_UDP => rpc::repl_udp(data, masscanned, &mut client_info, tcb),
+ PROTO_SMB1 => smb::repl_smb1(data, masscanned, &mut client_info, tcb),
+ PROTO_SMB2 => smb::repl_smb2(data, masscanned, &mut client_info, tcb),
+ _ => {
+ if let Some(t) = &mut tcb {
+ t.proto_id = PROTO_NONE;
+ }
+ None
+ }
}
- None
}
#[cfg(test)]
@@ -139,6 +199,8 @@ mod tests {
use pnet::util::MacAddr;
+ use crate::logger::MetaLogger;
+
#[test]
fn test_proto_dispatch_stun() {
let mut client_info = ClientInfo::new();
@@ -153,7 +215,9 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
/***** TEST STUN - MAGIC *****/
/* test payload is:
@@ -164,7 +228,7 @@ mod tests {
*/
let payload =
b"\x00\x01\x00\x00\x21\x12\xa4\x42\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
- let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) {
+ let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) {
r
} else {
panic!("expected an answer, got nothing");
@@ -178,7 +242,7 @@ mod tests {
*/
let payload =
b"\x00\x01\x00\x00\xaa\xbb\xcc\xdd\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
- let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) {
+ let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) {
r
} else {
panic!("expected an answer, got nothing");
@@ -191,7 +255,7 @@ mod tests {
*/
let payload =
b"\x00\x01\x00\x08\x01\xdb\xd4]4\x9f\xe2RQ\x19\x05,\x93\x14f4\x00\x03\x00\x04\x00\x00\x00\x00";
- let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) {
+ let _stun_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) {
r
} else {
panic!("expected an answer, got nothing");
@@ -212,28 +276,123 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
/***** TEST SSH *****/
let payloads = [
- "SSH-2.0-PUTTY",
- "SSH-2.0-Go",
- "SSH-2.0-libssh2_1.4.3",
- "SSH-2.0-PuTTY",
- "SSH-2.0-AsyncSSH_2.1.0",
- "SSH-2.0-libssh2_1.9.0",
- "SSH-2.0-libssh2_1.7.0",
- "SSH-2.0-8.35 FlowSsh: FlowSshNet_SftpStress54.38.116.473",
- "SSH-2.0-libssh_0.9.5",
- "SSH-2.0-OpenSSH_6.7p1 Raspbian-5+deb8u3",
+ "SSH-2.0-PUTTY\r\n",
+ "SSH-2.0-Go\r\n",
+ "SSH-2.0-libssh2_1.4.3\r\n",
+ "SSH-2.0-PuTTY\r\n",
+ "SSH-2.0-AsyncSSH_2.1.0\r\n",
+ "SSH-2.0-libssh2_1.9.0\r\n",
+ "SSH-2.0-libssh2_1.7.0\r\n",
+ "SSH-2.0-8.35 FlowSsh: FlowSshNet_SftpStress54.38.116.473\r\n",
+ "SSH-2.0-libssh_0.9.5\r\n",
+ "SSH-2.0-OpenSSH_6.7p1 Raspbian-5+deb8u3\r\n",
+ "SSH-1.99-Cisco-1.25\r\n",
];
for payload in payloads.iter() {
- let _ssh_resp = if let Some(r) = repl(payload.as_bytes(), &masscanned, &mut client_info)
- {
- r
- } else {
- panic!("expected an answer, got nothing");
- };
+ let _ssh_resp =
+ if let Some(r) = repl(payload.as_bytes(), &masscanned, &mut client_info, None) {
+ r
+ } else {
+ panic!("expected an answer, got nothing");
+ };
+ }
+ }
+
+ #[test]
+ fn test_proto_dispatch_ghost() {
+ let mut client_info = ClientInfo::new();
+ let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0);
+ client_info.ip.src = Some(IpAddr::V4(test_ip_addr));
+ client_info.port.src = Some(65000);
+ let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3);
+ let mut ips = HashSet::new();
+ ips.insert(IpAddr::V4(masscanned_ip_addr));
+ /* Construct masscanned context object */
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
+ iface: None,
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ /***** TEST GHOST *****/
+ let payloads = [
+ b"Gh0st\xad\x00\x00\x00\xe0\x00\x00\x00x\x9cKS``\x98\xc3\xc0\xc0\xc0\x06\xc4\x8c@\xbcQ\x96\x81\x81\tH\x07\xa7\x16\x95e&\xa7*\x04$&g+\x182\x94\xf6\xb000\xac\xa8rc\x00\x01\x11\xa0\x82\x1f\\`&\x83\xc7K7\x86\x19\xe5n\x0c9\x95n\x0c;\x84\x0f3\xac\xe8sch\xa8^\xcf4'J\x97\xa9\x82\xe30\xc3\x91h]&\x90\xf8\xce\x97S\xcbA4L?2=\xe1\xc4\x92\x86\x0b@\xf5`\x0cT\x1f\xae\xaf]\nr\x0b\x03#\xa3\xdc\x02~\x06\x86\x03+\x18m\xc2=\xfdtC,C\xfdL<<==\\\x9d\x19\x88\x00\xe5 \x02\x00T\xf5+\\"
+ ];
+ for payload in payloads.iter() {
+ let _ghost_resp =
+ if let Some(r) = repl(&payload.to_vec(), &masscanned, &mut client_info, None) {
+ r
+ } else {
+ panic!("expected an answer, got nothing");
+ };
+ }
+ }
+
+ #[test]
+ fn test_proto_dispatch_http() {
+ /* ensure that HTTP FSM does not answer until completion of request
+ * (at least headers) */
+ let mut client_info = ClientInfo::new();
+ let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0);
+ client_info.ip.src = Some(IpAddr::V4(test_ip_addr));
+ client_info.port.src = Some(65000);
+ let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3);
+ let mut ips = HashSet::new();
+ ips.insert(IpAddr::V4(masscanned_ip_addr));
+ /* Construct masscanned context object */
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
+ iface: None,
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ /***** TEST COMPLETE REQUEST *****/
+ let payload = b"GET / HTTP/1.1\r\n\r\n";
+ if let None = repl(&payload.to_vec(), &masscanned, &mut client_info, None) {
+ panic!("expected an answer, got nothing");
+ }
+ /***** TEST INCOMPLETE REQUEST *****/
+ let payload = b"GET / HTTP/1.1\r\n";
+ if let Some(_) = repl(&payload.to_vec(), &masscanned, &mut client_info, None) {
+ panic!("expected no answer, got one");
+ }
+ }
+
+ #[test]
+ fn dispatch_dns() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let mut client_info = ClientInfo::new();
+ client_info.ip.dst = Some(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)));
+ let payloads = [
+ b"\x04\xd2\x01\x00\x00\x03\x00\x00\x00\x00\x00\x00\x03www\x08example1\x03com\x00\x00\x01\x00\x01\x03www\x08example2\x03com\x00\x00\x01\x00\x01\x03www\x08example3\x03com\x00\x00\x01\x00\x01",
+ ];
+ for payload in payloads.iter() {
+ let dns_resp =
+ if let Some(r) = repl(&payload.to_vec(), &masscanned, &mut client_info, None) {
+ r
+ } else {
+ panic!("expected an answer, got nothing");
+ };
+ if let Err(e) = DNSPacket::try_from(dns_resp) {
+ panic!("error trying to parse the DNS answer: {}", e);
+ }
}
}
}
diff --git a/src/proto/rpc.rs b/src/proto/rpc.rs
new file mode 100644
index 0000000..932b2f5
--- /dev/null
+++ b/src/proto/rpc.rs
@@ -0,0 +1,561 @@
+// This file is part of masscanned.
+// Copyright 2021 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use log::warn;
+use std::convert::TryInto;
+use std::net::IpAddr;
+
+use crate::client::ClientInfo;
+use crate::proto::{ProtocolState as GenericProtocolState, TCPControlBlock};
+use crate::Masscanned;
+
+// last fragment (1 bit) + fragment len (31 bits) / length XID (random) / message type: call (0) / RPC version (0-255) / Program: Portmap (99840 - 100095) / Program version (*, random versions used, see below) / / Procedure: ??? (0-255)
+pub const RPC_CALL_TCP: &[u8; 28] =
+ b"********\x00\x00\x00\x00\x00\x00\x00*\x00\x01\x86*****\x00\x00\x00*";
+// UDP: last fragment and fragment len are missing
+pub const RPC_CALL_UDP: &[u8; 24] =
+ b"****\x00\x00\x00\x00\x00\x00\x00*\x00\x01\x86*****\x00\x00\x00*";
+
+#[derive(Debug)]
+enum RpcState {
+ Frag,
+ Xid,
+ MessageType,
+ RpcVersion,
+ Program,
+ ProgramVersion,
+ Procedure,
+ CredsFlavor,
+ CredsLen,
+ Creds,
+ VerifFlavor,
+ VerifLen,
+ Verif,
+ End,
+}
+
+#[derive(Debug)]
+pub struct ProtocolState {
+ state: RpcState,
+ last_frag: bool,
+ frag_len: u32,
+ xid: u32,
+ message_type: u32,
+ rpc_version: u32,
+ program: u32,
+ prog_version: u32,
+ procedure: u32,
+ creds_flavor: u32,
+ creds_data: Vec,
+ verif_flavor: u32,
+ verif_data: Vec,
+ payload: Vec,
+ cur_len: u32,
+ data_len: u32,
+}
+
+struct Rpcb {
+ program: u32,
+ version: u32,
+ netid: String,
+ addr: String,
+ port: u16,
+ owner: String,
+}
+
+impl ProtocolState {
+ fn new() -> Self {
+ ProtocolState {
+ state: RpcState::Frag,
+ last_frag: false,
+ frag_len: 0,
+ xid: 0,
+ message_type: 0,
+ rpc_version: 0,
+ program: 0,
+ prog_version: 0,
+ procedure: 0,
+ creds_flavor: 0,
+ creds_data: Vec::::new(),
+ verif_flavor: 0,
+ verif_data: Vec::::new(),
+ payload: Vec::::new(),
+ cur_len: 0,
+ data_len: 0,
+ }
+ }
+}
+
+fn read_u32(pstate: &mut ProtocolState, byte: u8, value: u32, next_state: RpcState) -> u32 {
+ pstate.cur_len += 1;
+ if pstate.cur_len == 4 {
+ pstate.state = next_state;
+ pstate.cur_len = 0;
+ }
+ value * 256 + byte as u32
+}
+
+fn read_string(pstate: &mut ProtocolState, next_state: RpcState) {
+ pstate.data_len -= 1;
+ if pstate.data_len == 0 {
+ pstate.state = next_state;
+ }
+}
+
+fn rpc_parse(pstate: &mut ProtocolState, data: &[u8]) {
+ for byte in data {
+ match pstate.state {
+ RpcState::Frag => {
+ if pstate.cur_len == 0 {
+ match byte & 128 {
+ 0 => pstate.last_frag = false,
+ _ => pstate.last_frag = true,
+ };
+ pstate.frag_len = (*byte & 127) as u32;
+ } else {
+ pstate.frag_len = *byte as u32;
+ }
+ pstate.cur_len += 1;
+ if pstate.cur_len == 4 {
+ pstate.state = RpcState::Xid;
+ pstate.cur_len = 0;
+ }
+ }
+ RpcState::Xid => {
+ pstate.xid = read_u32(pstate, *byte, pstate.xid, RpcState::MessageType)
+ }
+ RpcState::MessageType => {
+ pstate.message_type =
+ read_u32(pstate, *byte, pstate.message_type, RpcState::RpcVersion)
+ }
+ RpcState::RpcVersion => {
+ pstate.rpc_version = read_u32(pstate, *byte, pstate.rpc_version, RpcState::Program)
+ }
+ RpcState::Program => {
+ pstate.program = read_u32(pstate, *byte, pstate.program, RpcState::ProgramVersion)
+ }
+ RpcState::ProgramVersion => {
+ pstate.prog_version =
+ read_u32(pstate, *byte, pstate.prog_version, RpcState::Procedure)
+ }
+ RpcState::Procedure => {
+ pstate.procedure = read_u32(pstate, *byte, pstate.procedure, RpcState::CredsFlavor)
+ }
+ RpcState::CredsFlavor => {
+ pstate.creds_flavor =
+ read_u32(pstate, *byte, pstate.creds_flavor, RpcState::CredsLen)
+ }
+ RpcState::CredsLen => {
+ pstate.data_len = read_u32(pstate, *byte, pstate.data_len, RpcState::Creds);
+ if matches!(pstate.state, RpcState::Creds) && pstate.data_len == 0 {
+ pstate.state = RpcState::VerifFlavor
+ }
+ }
+ RpcState::Creds => {
+ pstate.creds_data.push(*byte);
+ read_string(pstate, RpcState::VerifFlavor)
+ }
+ RpcState::VerifFlavor => {
+ pstate.verif_flavor =
+ read_u32(pstate, *byte, pstate.verif_flavor, RpcState::VerifLen)
+ }
+ RpcState::VerifLen => {
+ pstate.data_len = read_u32(pstate, *byte, pstate.data_len, RpcState::Verif);
+ if matches!(pstate.state, RpcState::Verif) && pstate.cur_len == 0 {
+ pstate.state = RpcState::End
+ }
+ }
+ RpcState::Verif => {
+ pstate.verif_data.push(*byte);
+ read_string(pstate, RpcState::End)
+ }
+ RpcState::End => {
+ pstate.payload.push(*byte);
+ }
+ };
+ }
+}
+
+fn get_nth_byte(value: u32, nth: u8) -> u8 {
+ let shift = 8 * (3 - nth);
+ ((value & (0xff << shift)) >> shift).try_into().unwrap()
+}
+
+fn push_u32(buffer: &mut Vec, data: u32) {
+ for i in 0..4 {
+ buffer.push(get_nth_byte(data, i));
+ }
+}
+
+fn push_string_pad(buffer: &mut Vec, data: String) {
+ let len: u32 = data.len().try_into().unwrap();
+ push_u32(buffer, len);
+ buffer.append(&mut data.as_bytes().to_vec());
+ if len % 4 != 0 {
+ for _ in 0..(4 - (len % 4)) {
+ buffer.append(&mut b"\x00".to_vec());
+ }
+ }
+}
+
+fn build_repl_portmap(pstate: &mut ProtocolState, client_info: &ClientInfo) -> Vec {
+ let mut resp = Vec::::new();
+ match pstate.procedure {
+ // 0 => {}
+ 3 => {
+ // getaddr / getport
+ // accepted state: 0 (RPC executed successfully)
+ resp.extend([0, 0, 0, 0]);
+ let localport = client_info.port.dst.unwrap();
+ match pstate.prog_version {
+ 2 => {
+ push_u32(&mut resp, localport as u32);
+ }
+ 3 | 4 => {
+ let addr = format!(
+ "{}.{}.{}",
+ client_info.ip.dst.unwrap(),
+ localport >> 8,
+ localport % 256
+ );
+ push_string_pad(&mut resp, addr);
+ }
+ _ => panic!("Wrong RPC version"),
+ }
+ }
+ 4 => {
+ // dump
+ // accepted state: 0 (RPC executed successfully)
+ resp.extend([0, 0, 0, 0]);
+ let localaddr = client_info.ip.dst.unwrap();
+ let localport = client_info.port.dst.unwrap();
+ let netid = match localaddr {
+ IpAddr::V4(_) => "tcp",
+ IpAddr::V6(_) => "tcp6",
+ };
+ for rpcb in [
+ Rpcb {
+ program: 100000,
+ version: 2,
+ netid: netid.to_string(),
+ addr: format!("{}", localaddr),
+ port: localport,
+ owner: "superuser".to_string(),
+ },
+ Rpcb {
+ program: 100000,
+ version: 3,
+ netid: netid.to_string(),
+ addr: format!("{}", localaddr),
+ port: localport,
+ owner: "superuser".to_string(),
+ },
+ Rpcb {
+ program: 100000,
+ version: 4,
+ netid: netid.to_string(),
+ addr: format!("{}", localaddr),
+ port: localport,
+ owner: "superuser".to_string(),
+ },
+ ] {
+ resp.append(&mut b"\x00\x00\x00\x01".to_vec()); // value follows: yes
+ push_u32(&mut resp, rpcb.program);
+ push_u32(&mut resp, rpcb.version);
+ match pstate.prog_version {
+ 2 => {
+ push_u32(
+ &mut resp,
+ match rpcb.netid.as_str() {
+ "tcp" => 6,
+ "tcp6" => 6,
+ "udp" => 17,
+ "udp6" => 17,
+ _ => 0,
+ },
+ );
+ push_u32(&mut resp, localport as u32);
+ }
+ 3 | 4 => {
+ push_string_pad(&mut resp, rpcb.netid);
+ push_string_pad(
+ &mut resp,
+ format!("{}.{}.{}", rpcb.addr, rpcb.port >> 8, rpcb.port & 0xff),
+ );
+ push_string_pad(&mut resp, rpcb.owner);
+ }
+ _ => panic!("Wrong RPC version"),
+ }
+ }
+ resp.append(&mut b"\x00\x00\x00\x00".to_vec()); // value follows: no
+ }
+ _ => {
+ // accepted state: 5 (program can't support procedure)
+ resp.extend([0, 0, 0, 5]);
+ }
+ }
+ warn!(
+ "RPC: Portmap version {}, procedure {}",
+ pstate.prog_version, pstate.procedure
+ );
+ resp
+}
+
+fn build_repl_unknownprog(pstate: &mut ProtocolState, _client_info: &ClientInfo) -> Vec {
+ warn!(
+ "Unknown program {}, procedure {}: accepted state 1",
+ pstate.program, pstate.procedure
+ );
+ // accepted state: 1 (remote hasn't exported program)
+ vec![0, 0, 0, 1]
+}
+
+fn build_repl(pstate: &mut ProtocolState, client_info: &ClientInfo) -> Vec {
+ // TODO: test RPC versions, drop non calls?
+ let mut resp = Vec::::new();
+ push_u32(&mut resp, pstate.xid);
+ // message_type: 1 (reply)
+ // reply_state: 0 (accepted)
+ // verifier: 0 (auth null)
+ // verifier length: 0
+ resp.extend([0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
+ if pstate.prog_version < 2 || pstate.prog_version > 4 {
+ /*
+ * Scanners (e.g., Nmap script rpc-grind) often use random
+ * values for program version to find out if a program is
+ * supported, so for any program, we answer with "remote can't
+ * support version" accepted state.
+ */
+ // accepted state: 2 (remote can't support version)
+ // prog_version min: 2
+ // prog_version max: 4
+ let prog_version = match pstate.prog_version {
+ 104316 => "104316 (Nmap probe TCP RPCCheck)".to_string(),
+ x => x.to_string(),
+ };
+ warn!(
+ "RPC: unsupported version {} for program {}",
+ prog_version, pstate.program
+ );
+ resp.extend([0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 4]);
+ } else if pstate.procedure == 0 {
+ /*
+ * RPC clients (e.g., Linux kernel NFS client, rpcbind CLI
+ * tool) would often send a NULL procedure (0) call before any
+ * real operation .
+ */
+ // accepted state: 0 (RPC executed successfully)
+ warn!("RPC: NULL procedure call for program {}", pstate.program);
+ resp.extend([0, 0, 0, 0]);
+ } else {
+ let mut specif_resp = match pstate.program {
+ 100000 => build_repl_portmap(pstate, client_info),
+ _ => build_repl_unknownprog(pstate, client_info),
+ };
+ resp.append(&mut specif_resp);
+ }
+ resp
+}
+
+pub fn repl_tcp<'a>(
+ data: &'a [u8],
+ _masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ tcb: Option<&mut TCPControlBlock>,
+) -> Option> {
+ let mut state = ProtocolState::new();
+ let mut pstate = {
+ if let Some(t) = tcb {
+ match t.proto_state {
+ None => t.proto_state = Some(GenericProtocolState::RPC(ProtocolState::new())),
+ Some(GenericProtocolState::RPC(_)) => {}
+ _ => {
+ panic!()
+ }
+ };
+ if let Some(GenericProtocolState::RPC(p)) = &mut t.proto_state {
+ p
+ } else {
+ panic!();
+ }
+ } else {
+ &mut state
+ }
+ };
+ rpc_parse(&mut pstate, data);
+ // warn!("RPC {:#?}", pstate);
+ let resp = match pstate.state {
+ RpcState::End => Some(build_repl(pstate, client_info)),
+ _ => None,
+ };
+ match resp {
+ Some(mut resp) => {
+ let length: u32 = resp.len().try_into().unwrap();
+ let mut final_resp = Vec::::new();
+ for i in 0..4 {
+ match i {
+ 0 => final_resp.push(get_nth_byte(length, i) | 0x80),
+ _ => final_resp.push(get_nth_byte(length, i)),
+ };
+ }
+ final_resp.append(&mut resp);
+ Some(final_resp)
+ }
+ _ => None,
+ }
+}
+
+pub fn repl_udp<'a>(
+ data: &'a [u8],
+ _masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+) -> Option> {
+ let mut pstate = ProtocolState::new();
+ pstate.state = RpcState::Xid;
+ pstate.last_frag = true;
+ pstate.frag_len = data.len().try_into().unwrap();
+ rpc_parse(&mut pstate, data);
+ // warn!("RPC {:#?}", pstate);
+ match pstate.state {
+ RpcState::End => Some(build_repl(&mut pstate, client_info)),
+ _ => None,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::client::ClientInfoSrcDst;
+ use std::net::Ipv4Addr;
+
+ const CLIENT_INFO: ClientInfo = ClientInfo {
+ mac: ClientInfoSrcDst {
+ src: None,
+ dst: None,
+ },
+ ip: ClientInfoSrcDst {
+ src: Some(IpAddr::V4(Ipv4Addr::new(192, 0, 0, 0))),
+ dst: Some(IpAddr::V4(Ipv4Addr::new(192, 0, 0, 1))),
+ },
+ transport: None,
+ port: ClientInfoSrcDst {
+ src: Some(12345),
+ dst: Some(111),
+ },
+ cookie: None,
+ };
+
+ #[test]
+ fn test_probe_nmap() {
+ let mut pstate = ProtocolState::new();
+ rpc_parse(&mut pstate, b"\x80\x00\x00\x28\x72\xfe\x1d\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x01\x97\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00");
+ assert!(matches!(pstate.state, RpcState::End));
+ assert!(pstate.xid == 0x72fe1d13);
+ assert!(pstate.rpc_version == 2);
+ assert!(pstate.program == 100000);
+ assert!(pstate.prog_version == 104316);
+ assert!(pstate.procedure == 0);
+ assert!(pstate.creds_flavor == 0);
+ assert!(pstate.creds_data.len() == 0);
+ assert!(pstate.verif_flavor == 0);
+ assert!(pstate.verif_data.len() == 0);
+ let resp = build_repl(&mut pstate, &CLIENT_INFO);
+ assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04");
+ }
+
+ #[test]
+ fn test_probe_nmap_udp() {
+ let mut pstate = ProtocolState::new();
+ pstate.state = RpcState::Xid;
+ rpc_parse(&mut pstate, b"\x72\xfe\x1d\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x01\x97\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00");
+ assert!(matches!(pstate.state, RpcState::End));
+ assert!(pstate.xid == 0x72fe1d13);
+ assert!(pstate.rpc_version == 2);
+ assert!(pstate.program == 100000);
+ assert!(pstate.prog_version == 104316);
+ assert!(pstate.procedure == 0);
+ assert!(pstate.creds_flavor == 0);
+ assert!(pstate.creds_data.len() == 0);
+ assert!(pstate.verif_flavor == 0);
+ assert!(pstate.verif_data.len() == 0);
+ let resp = build_repl(&mut pstate, &CLIENT_INFO);
+ assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04");
+ }
+
+ #[test]
+ fn test_probe_nmap_split1() {
+ let mut pstate = ProtocolState::new();
+ for byte in b"\x80\x00\x00\x28\x72\xfe\x1d\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x01\x97\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" {
+ rpc_parse(&mut pstate, &[*byte]);
+ }
+ assert!(matches!(pstate.state, RpcState::End));
+ assert!(pstate.xid == 0x72fe1d13);
+ assert!(pstate.rpc_version == 2);
+ assert!(pstate.program == 100000);
+ assert!(pstate.prog_version == 104316);
+ assert!(pstate.procedure == 0);
+ assert!(pstate.creds_flavor == 0);
+ assert!(pstate.creds_data.len() == 0);
+ assert!(pstate.verif_flavor == 0);
+ assert!(pstate.verif_data.len() == 0);
+ let resp = build_repl(&mut pstate, &CLIENT_INFO);
+ assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04");
+ }
+
+ #[test]
+ fn test_probe_nmap_split2() {
+ let mut pstate = ProtocolState::new();
+ for data in [
+ b"\x80\x00\x00\x28\x72\xfe\x1d",
+ b"\x13\x00\x00\x00\x00\x00\x00",
+ b"\x00\x02\x00\x01\x86\xa0\x00",
+ b"\x01\x97\x7c\x00\x00\x00\x00",
+ b"\x00\x00\x00\x00\x00\x00\x00",
+ b"\x00\x00\x00\x00\x00\x00\x00",
+ ] {
+ rpc_parse(&mut pstate, data);
+ }
+ rpc_parse(&mut pstate, b"\x00\x00");
+ assert!(matches!(pstate.state, RpcState::End));
+ assert!(pstate.xid == 0x72fe1d13);
+ assert!(pstate.rpc_version == 2);
+ assert!(pstate.program == 100000);
+ assert!(pstate.prog_version == 104316);
+ assert!(pstate.procedure == 0);
+ assert!(pstate.creds_flavor == 0);
+ assert!(pstate.creds_data.len() == 0);
+ assert!(pstate.verif_flavor == 0);
+ assert!(pstate.verif_data.len() == 0);
+ let resp = build_repl(&mut pstate, &CLIENT_INFO);
+ assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04");
+ }
+
+ #[test]
+ fn test_probe_portmap_v4_dump() {
+ let mut pstate = ProtocolState::new();
+ rpc_parse(&mut pstate, b"\x80\x00\x00\x28\x01\x1b\x60\xa6\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x00\x00\x04\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00");
+ assert!(matches!(pstate.state, RpcState::End));
+ assert!(pstate.rpc_version == 2);
+ assert!(pstate.program == 100000);
+ assert!(pstate.prog_version == 4);
+ assert!(pstate.procedure == 4); // dump
+ assert!(pstate.creds_flavor == 0);
+ assert!(pstate.creds_data.len() == 0);
+ assert!(pstate.verif_flavor == 0);
+ assert!(pstate.verif_data.len() == 0);
+ }
+}
diff --git a/src/proto/smb.rs b/src/proto/smb.rs
new file mode 100644
index 0000000..41f9519
--- /dev/null
+++ b/src/proto/smb.rs
@@ -0,0 +1,1418 @@
+// This file is part of masscanned.
+// Copyright 2021 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use log::*;
+use std::collections::HashSet;
+use std::convert::TryInto;
+use std::time::SystemTime;
+
+use crate::client::ClientInfo;
+use crate::proto::TCPControlBlock;
+use crate::Masscanned;
+
+use crate::proto::dissector::{MPacket, PacketDissector};
+
+// NBTSession + SMB Header
+// netbios type (1 byte) + reserved (1 byte) + length (2 bytes) + SMB MAGIC (4 bytes)
+//
+pub const SMB1_PATTERN_MAGIC: &[u8; 8] = b"\x00\x00**\xffSMB";
+pub const SMB2_PATTERN_MAGIC: &[u8; 8] = b"\x00\x00**\xfeSMB";
+
+// Build/Dissect secblob with Scapy using: GSSAPI_BLOB(b"`\x82.....")
+const SECURITY_BLOB_NEG_PROTO: &[u8] = b"`\x82\x01<\x06\x06+\x06\x01\x05\x05\x02\xa0\x82\x0100\x82\x01,\xa0\x1a0\x18\x06\n+\x06\x01\x04\x01\x827\x02\x02\x1e\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2\x82\x01\x0c\x04\x82\x01\x08NEGOEXTS\x01\x00\x00\x00\x00\x00\x00\x00`\x00\x00\x00p\x00\x00\x001<*:\xc7+<\xa9m\xac8t\xa7\xdd\x1d[\xf4Rk\x17\x03\x8aK\x91\xc2\t}\x9a\x8f\xe6,\x96\\Q$/\x90MG\xc7\xad\x8f\x87k\"\x02\xbf\xc6\x00\x00\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\\3S\r\xea\xf9\rM\xb2\xecJ\xe3xn\xc3\x08NEGOEXTS\x03\x00\x00\x00\x01\x00\x00\x00@\x00\x00\x00\x98\x00\x00\x001<*:\xc7+<\xa9m\xac8t\xa7\xdd\x1d[\\3S\r\xea\xf9\rM\xb2\xecJ\xe3xn\xc3\x08@\x00\x00\x00X\x00\x00\x000V\xa0T0R0'\x80%0#1!0\x1f\x06\x03U\x04\x03\x13\x18Token Signing Public Key0'\x80%0#1!0\x1f\x06\x03U\x04\x03\x13\x18Token Signing Public Key";
+const SECURITY_BLOB_CHALLENGE: &[u8] = b"\xa1\x81\x9c0\x81\x99\xa0\x03\n\x01\x01\xa1\x0c\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2\x81\x83\x04\x81\x80NTLMSSP\x00\x02\x00\x00\x00\x08\x00\x08\x008\x00\x00\x00\x15\x82\x8a\xe2$\x91\xa8\xf6\xf3\x89-4\x00\x00\x00\x00\x00\x00\x00\x00@\x00@\x00@\x00\x00\x00\n\x00aJ\x00\x00\x00\x0fW\x00I\x00N\x001\x00\x02\x00\x08\x00W\x00I\x00N\x001\x00\x01\x00\x08\x00W\x00I\x00N\x001\x00\x04\x00\x08\x00W\x00I\x00N\x001\x00\x03\x00\x08\x00W\x00I\x00N\x001\x00\x07\x00\x08\x00\xff&9\xf5B\x1d\xd8\x01\x00\x00\x00\x00";
+
+/////////////
+// Netbios //
+/////////////
+
+#[derive(Debug, Clone, Copy)]
+enum NBTSessionState {
+ NBType,
+ Reserved,
+ Length,
+ End,
+}
+
+#[derive(Debug, Clone)]
+struct NBTSession {
+ // DISSECTION
+ d: PacketDissector,
+ // STRUCT
+ nb_type: u8,
+ length: u16,
+ payload: Option,
+}
+
+impl MPacket for NBTSession {
+ fn new() -> NBTSession {
+ Self {
+ d: PacketDissector::new(NBTSessionState::NBType),
+ nb_type: 0,
+ length: 0,
+ payload: None,
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ NBTSessionState::NBType => {
+ self.nb_type = *byte;
+ self.d.next_state(NBTSessionState::Reserved);
+ }
+ NBTSessionState::Reserved => {
+ self.d.next_state(NBTSessionState::Length);
+ }
+ NBTSessionState::Length => {
+ self.length = self.d.read_u16(byte, self.length, NBTSessionState::End)
+ }
+ NBTSessionState::End => match self.get_payload() {
+ Some(pay) => pay.parse(byte),
+ None => return,
+ },
+ }
+ }
+
+ fn repl(
+ &self,
+ masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ let payload_resp = self.payload.as_ref()?.repl(masscanned, client_info, tcb)?;
+ let mut resp: Vec = Vec::new();
+ let size = payload_resp.len() & 0x1ffff; // 7 first bits are 0
+ resp.push(0x0);
+ // 7 bits reserved + 17 bits length
+ resp.push(((size as u32 >> 16) & 0xff).try_into().unwrap());
+ resp.extend_from_slice(&((size & 0xffff) as u16).to_be_bytes());
+ resp.extend(payload_resp);
+ Some(resp)
+ }
+}
+
+impl NBTSession {
+ fn get_payload(&mut self) -> Option<&mut T> {
+ if self.payload.is_some() {
+ return self.payload.as_mut();
+ }
+ self.payload = Some(T::new());
+ self.payload.as_mut()
+ }
+}
+
+//////////
+// SMB1 //
+//////////
+
+#[derive(Debug, Clone, Copy)]
+enum SMB1HeaderState {
+ Start,
+ Command,
+ Status,
+ Flags,
+ Flags2,
+ PIDHigh,
+ SecuritySignature,
+ Reserved,
+ TID,
+ PIDLow,
+ UID,
+ MID,
+ End,
+}
+
+#[derive(Debug, Clone)]
+struct SMB1Header {
+ // DISSECTION
+ d: PacketDissector,
+ // STRUCT
+ start: [u8; 4],
+ command: u8,
+ status: u32,
+ flags: u8,
+ flags2: u16,
+ pid_high: u16,
+ security_signature: [u8; 8],
+ tid: u16,
+ pid_low: u16,
+ uid: u16,
+ mid: u16,
+ payload: Option,
+}
+
+impl MPacket for SMB1Header {
+ fn new() -> SMB1Header {
+ Self {
+ d: PacketDissector::new(SMB1HeaderState::Start),
+ start: [0; 4],
+ command: 0,
+ status: 0,
+ flags: 0,
+ flags2: 0,
+ pid_high: 0,
+ security_signature: [0; 8],
+ tid: 0,
+ pid_low: 0,
+ uid: 0,
+ mid: 0,
+ payload: None,
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ SMB1HeaderState::Start => {
+ self.start[self.d.i] = *byte;
+ self.d.i += 1;
+ self.d
+ .next_state_when_i_reaches(SMB1HeaderState::Command, 4);
+ }
+ SMB1HeaderState::Command => {
+ self.command = *byte;
+ self.d.next_state(SMB1HeaderState::Status);
+ }
+ SMB1HeaderState::Status => {
+ self.status = self.d.read_ule32(byte, self.status, SMB1HeaderState::Flags);
+ }
+ SMB1HeaderState::Flags => {
+ self.flags = *byte;
+ self.d.next_state(SMB1HeaderState::Flags2);
+ }
+ SMB1HeaderState::Flags2 => {
+ self.flags2 = self
+ .d
+ .read_ule16(byte, self.flags2, SMB1HeaderState::PIDHigh);
+ }
+ SMB1HeaderState::PIDHigh => {
+ self.pid_high =
+ self.d
+ .read_ule16(byte, self.pid_high, SMB1HeaderState::SecuritySignature);
+ }
+ SMB1HeaderState::SecuritySignature => {
+ self.security_signature[self.d.i] = *byte;
+ self.d.i += 1;
+ self.d
+ .next_state_when_i_reaches(SMB1HeaderState::Reserved, 8);
+ }
+ SMB1HeaderState::Reserved => {
+ self.d.i += 1;
+ self.d.next_state_when_i_reaches(SMB1HeaderState::TID, 2);
+ }
+ SMB1HeaderState::TID => {
+ self.tid = self.d.read_ule16(byte, self.tid, SMB1HeaderState::PIDLow);
+ }
+ SMB1HeaderState::PIDLow => {
+ self.pid_low = self.d.read_ule16(byte, self.pid_low, SMB1HeaderState::UID);
+ }
+ SMB1HeaderState::UID => {
+ self.uid = self.d.read_ule16(byte, self.uid, SMB1HeaderState::MID);
+ }
+ SMB1HeaderState::MID => {
+ self.mid = self.d.read_ule16(byte, self.mid, SMB1HeaderState::End);
+ }
+ SMB1HeaderState::End => match self.get_payload() {
+ Some(pay) => pay.parse(byte),
+ None => return,
+ },
+ }
+ }
+
+ fn repl(
+ &self,
+ masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ let payload_resp = self.payload.as_ref()?.repl(masscanned, client_info, tcb)?;
+ let mut resp: Vec = Vec::new();
+ resp.extend_from_slice(b"\xffSMB"); // Start
+ resp.push(self.command); // Command
+ resp.extend_from_slice(&0_u32.to_le_bytes()); // Status
+ resp.push(0x98); // Flags = CASE_INSENSITIVE+CANONICALIZED_PATHS+REPLY
+ resp.extend_from_slice(&0xc807_u16.to_le_bytes()); // Flags2 = LONG_NAMES+EAS+SMB_SECURITY_SIGNATURE+EXTENDED_SECURITY+NT_STATUS+UNICODE
+ resp.extend_from_slice(&self.pid_high.to_le_bytes()); // PIDHigh
+ resp.extend_from_slice(&[0; 8]); // SecuritySignature
+ resp.extend_from_slice(&[0; 2]); // Reserved
+ resp.extend_from_slice(&self.tid.to_le_bytes()); // TID
+ resp.extend_from_slice(&self.pid_low.to_le_bytes()); // PIDLOW
+ resp.extend_from_slice(&self.uid.to_le_bytes()); // UID
+ resp.extend_from_slice(&self.mid.to_le_bytes()); // MID
+ resp.extend(payload_resp);
+ Some(resp)
+ }
+}
+
+impl SMB1Header {
+ fn get_payload(&mut self) -> Option<&mut SMB1Payload> {
+ if self.payload.is_some() {
+ return self.payload.as_mut();
+ }
+ if self.flags & 0x80 == 0x80 {
+ // Response
+ return None;
+ }
+ self.payload = Some(match self.command {
+ 0x72 => {
+ // Negotiate
+ SMB1Payload::NegotiateRequest(SMB1NegotiateRequest::new())
+ }
+ 0x73 => {
+ // Setup
+ SMB1Payload::SessionSetupRequest(SMB1SessionSetupRequest::new())
+ }
+ _ => None?,
+ });
+ self.payload.as_mut()
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+struct SMB1Dialect {
+ buffer_format: u8,
+ dialect_string: String,
+}
+
+#[derive(Debug, Clone, Copy)]
+enum SMB1NegotiateRequestState {
+ WordCount,
+ ByteCount,
+ Dialects,
+ End,
+}
+
+#[derive(Debug, Clone)]
+struct SMB1NegotiateRequest {
+ // DISSECTION
+ d: PacketDissector,
+ _tmp_dialect: Option,
+ // STRUCT
+ word_count: u8,
+ byte_count: u16,
+ dialects: Vec,
+}
+
+impl MPacket for SMB1NegotiateRequest {
+ fn new() -> SMB1NegotiateRequest {
+ Self {
+ d: PacketDissector::new(SMB1NegotiateRequestState::WordCount),
+ _tmp_dialect: None,
+ word_count: 0,
+ byte_count: 0,
+ dialects: Vec::new(),
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ SMB1NegotiateRequestState::WordCount => {
+ self.word_count = *byte;
+ self.d.next_state(SMB1NegotiateRequestState::ByteCount);
+ }
+ SMB1NegotiateRequestState::ByteCount => {
+ self.byte_count =
+ self.d
+ .read_ule16(byte, self.byte_count, SMB1NegotiateRequestState::Dialects);
+ }
+ SMB1NegotiateRequestState::Dialects => {
+ self.d.i += 1;
+ match self._tmp_dialect.as_mut() {
+ Some(dial) => {
+ if *byte == 0 {
+ // Final nul byte: dialect is finished
+ self.dialects.push(dial.clone());
+ self._tmp_dialect = None;
+ self.d.next_state_when_i_reaches(
+ SMB1NegotiateRequestState::End,
+ self.byte_count as usize,
+ );
+ } else {
+ dial.dialect_string.push(*byte as char);
+ }
+ }
+ None => {
+ self._tmp_dialect = Some(SMB1Dialect {
+ buffer_format: *byte,
+ dialect_string: String::new(),
+ });
+ }
+ }
+ }
+ SMB1NegotiateRequestState::End => {}
+ }
+ }
+
+ fn repl(
+ &self,
+ _masscanned: &Masscanned,
+ _client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ if !matches!(self.d.state, SMB1NegotiateRequestState::End) {
+ return None;
+ }
+ let mut resp: Vec = Vec::new();
+ let time: u64 = (EPOCH_1601
+ + SystemTime::now()
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .unwrap()
+ .as_secs())
+ * (1e7 as u64);
+ let mut dialect_index: u16 = 0;
+ let mut dialect_name = "Unknown";
+ for dialect in ["NT LM 0.12", "SMB 2.???", "SMB 2.002"] {
+ dialect_index = match self
+ .dialects
+ .iter()
+ .position(|x| x.dialect_string.eq(dialect))
+ {
+ Some(x) => {
+ dialect_name = dialect;
+ x as u16
+ }
+ None => continue,
+ };
+ break;
+ }
+ resp.push(17); // WordCount
+ resp.extend_from_slice(&dialect_index.to_le_bytes()); // DialectIndex
+ resp.push(3); // SecurityMode
+ resp.extend_from_slice(&50_u16.to_le_bytes()); // MaxMPXCount
+ resp.extend_from_slice(&50_u16.to_le_bytes()); // MaxNumberVC
+ resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxBufferSize
+ resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxRawSize
+ resp.extend_from_slice(&0x0_u32.to_le_bytes()); // SessionKey
+ resp.extend_from_slice(&0x8001e3fc_u32.to_le_bytes()); // ServerCapabilities = UNICODE+LARGE_FILES+NT_SMBS+RPC_REMOTE_APIS+STATUS32+LEVEL_II_OPLOCKS+LOCK_AND_READ+NT_FIND+INFOLEVEL_PASSTHRU+LARGE_READX+LARGE_WRITEX+LWIO+EXTENDED_SECURITY
+ resp.extend_from_slice(&time.to_le_bytes()); // ServerTime
+ resp.extend_from_slice(&0x3c_u16.to_le_bytes()); // ServerTimeZone
+ resp.push(0); // ChallengeLength
+ resp.extend_from_slice(&((SECURITY_BLOB_NEG_PROTO.len() + 16) as u16).to_le_bytes()); // ByteCount
+ // Challenge: Empty
+ resp.extend_from_slice(&[0_u8; 16]); // GUID
+ resp.extend_from_slice(SECURITY_BLOB_NEG_PROTO); // SecurityBlob
+ warn!("SMB1 Negotiate-Protocol-Reply ({})", dialect_name);
+ Some(resp)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+enum SMB1SessionSetupRequestState {
+ WordCount,
+ AndXCommand,
+ AndXReserved,
+ AndXOffset,
+ MaxBufferSize,
+ MaxMPXCount,
+ VcNumber,
+ SessionKey,
+ SecurityBlobLength,
+ Reserved,
+ ServerCapabilities,
+ ByteCount,
+ SecurityBlob,
+ End,
+}
+
+#[derive(Debug, Clone)]
+struct SMB1SessionSetupRequest {
+ // DISSECTION
+ d: PacketDissector,
+ // STRUCT
+ word_count: u8,
+ and_x_command: u8,
+ and_x_offset: u16,
+ max_buffer_size: u16,
+ max_mpx_count: u16,
+ vc_number: u16,
+ session_key: u32,
+ security_len: u16,
+ server_capabilities: u32,
+ byte_count: u16,
+}
+
+impl MPacket for SMB1SessionSetupRequest {
+ fn new() -> SMB1SessionSetupRequest {
+ Self {
+ d: PacketDissector::new(SMB1SessionSetupRequestState::WordCount),
+ word_count: 0,
+ and_x_command: 0,
+ and_x_offset: 0,
+ max_buffer_size: 0,
+ max_mpx_count: 0,
+ vc_number: 0,
+ session_key: 0,
+ security_len: 0,
+ server_capabilities: 0,
+ byte_count: 0,
+ }
+ }
+ fn parse(&mut self, byte: &u8) {
+ // We expect extended security because that's what we asked for in the NegotiateRequest
+ match self.d.state {
+ SMB1SessionSetupRequestState::WordCount => {
+ self.word_count = *byte;
+ self.d.next_state(SMB1SessionSetupRequestState::AndXCommand);
+ }
+ SMB1SessionSetupRequestState::AndXCommand => {
+ self.and_x_command = *byte;
+ self.d
+ .next_state(SMB1SessionSetupRequestState::AndXReserved);
+ }
+ SMB1SessionSetupRequestState::AndXReserved => {
+ self.d.next_state(SMB1SessionSetupRequestState::AndXOffset);
+ }
+ SMB1SessionSetupRequestState::AndXOffset => {
+ self.and_x_offset = self.d.read_ule16(
+ byte,
+ self.and_x_offset,
+ SMB1SessionSetupRequestState::MaxBufferSize,
+ );
+ }
+ SMB1SessionSetupRequestState::MaxBufferSize => {
+ self.max_buffer_size = self.d.read_ule16(
+ byte,
+ self.max_buffer_size,
+ SMB1SessionSetupRequestState::MaxMPXCount,
+ );
+ }
+ SMB1SessionSetupRequestState::MaxMPXCount => {
+ self.max_mpx_count = self.d.read_ule16(
+ byte,
+ self.max_mpx_count,
+ SMB1SessionSetupRequestState::VcNumber,
+ );
+ }
+ SMB1SessionSetupRequestState::VcNumber => {
+ self.vc_number = self.d.read_ule16(
+ byte,
+ self.vc_number,
+ SMB1SessionSetupRequestState::SessionKey,
+ );
+ }
+ SMB1SessionSetupRequestState::SessionKey => {
+ self.session_key = self.d.read_ule32(
+ byte,
+ self.session_key,
+ SMB1SessionSetupRequestState::SecurityBlobLength,
+ );
+ }
+ SMB1SessionSetupRequestState::SecurityBlobLength => {
+ self.security_len = self.d.read_ule16(
+ byte,
+ self.security_len,
+ SMB1SessionSetupRequestState::Reserved,
+ );
+ }
+ SMB1SessionSetupRequestState::Reserved => {
+ self.d.i += 1;
+ self.d
+ .next_state_when_i_reaches(SMB1SessionSetupRequestState::ServerCapabilities, 4);
+ }
+ SMB1SessionSetupRequestState::ServerCapabilities => {
+ self.server_capabilities = self.d.read_ule32(
+ byte,
+ self.server_capabilities,
+ SMB1SessionSetupRequestState::ByteCount,
+ );
+ }
+ SMB1SessionSetupRequestState::ByteCount => {
+ self.byte_count = self.d.read_ule16(
+ byte,
+ self.byte_count,
+ SMB1SessionSetupRequestState::SecurityBlob,
+ );
+ }
+ SMB1SessionSetupRequestState::SecurityBlob => {
+ self.d.i += 1;
+ self.d.next_state_when_i_reaches(
+ SMB1SessionSetupRequestState::End,
+ self.security_len as usize,
+ );
+ }
+ SMB1SessionSetupRequestState::End => {}
+ }
+ }
+ fn repl(
+ &self,
+ _masscanned: &Masscanned,
+ _client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ if !matches!(self.d.state, SMB1SessionSetupRequestState::End) {
+ return None;
+ }
+ // "Windows 4.0" in UTF-16 + two null bytes
+ let native_os = b"W\x00i\x00n\x00d\x00o\x00w\x00s\x00 \x004\x00.\x000\x00\x00\x00";
+ let native_man_lan = native_os;
+ let mut resp: Vec = Vec::new();
+ resp.push(0x4); // WordCount
+ resp.push(0xff); // AndXCommand
+ resp.push(0x0); // AndXReserved
+ resp.extend_from_slice(&0x44_u16.to_le_bytes()); // AndXOffset
+ resp.extend_from_slice(&0x0_u16.to_le_bytes()); // Action
+ resp.extend_from_slice(&(SECURITY_BLOB_CHALLENGE.len() as u16).to_le_bytes()); // SecurityLen
+ resp.extend_from_slice(
+ &((SECURITY_BLOB_CHALLENGE.len() + native_os.len() + native_man_lan.len()) as u16)
+ .to_le_bytes(),
+ ); // ByteCount
+ resp.extend_from_slice(SECURITY_BLOB_CHALLENGE); // SecurityBlob
+ resp.extend_from_slice(native_os);
+ resp.extend_from_slice(native_man_lan);
+ warn!("SMB1 SessionSetup-Reply");
+ Some(resp)
+ }
+}
+
+#[derive(Debug, Clone)]
+enum SMB1Payload {
+ NegotiateRequest(SMB1NegotiateRequest),
+ SessionSetupRequest(SMB1SessionSetupRequest),
+}
+
+impl SMB1Payload {
+ fn repl(
+ &self,
+ masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ match self {
+ SMB1Payload::NegotiateRequest(x) => x.repl(masscanned, client_info, tcb),
+ SMB1Payload::SessionSetupRequest(x) => x.repl(masscanned, client_info, tcb),
+ }
+ }
+ fn parse(&mut self, byte: &u8) {
+ match self {
+ SMB1Payload::NegotiateRequest(x) => x.parse(byte),
+ SMB1Payload::SessionSetupRequest(x) => x.parse(byte),
+ }
+ }
+}
+
+//////////
+// SMB2 //
+//////////
+
+#[derive(Debug, Clone, Copy)]
+enum SMB2HeaderState {
+ Start,
+ StructureSize,
+ CreditsCharge,
+ Status,
+ Command,
+ CreditsRequested,
+ Flags,
+ NextCommand,
+ MessageId,
+ AsyncId,
+ SessionId,
+ SecuritySignature,
+ End,
+}
+
+#[derive(Debug, Clone)]
+struct SMB2Header {
+ // DISSECTION
+ d: PacketDissector,
+ // STRUCT
+ start: [u8; 4],
+ structure_size: u16,
+ credit_charge: u16,
+ status: u32,
+ command: u16,
+ credits_requested: u16,
+ flags: u32,
+ next_command: u32,
+ message_id: u64,
+ async_id: u64,
+ session_id: u64,
+ security_signature: [u8; 16],
+ // Payload
+ payload: Option,
+}
+
+impl MPacket for SMB2Header {
+ fn new() -> SMB2Header {
+ SMB2Header {
+ d: PacketDissector::new(SMB2HeaderState::Start),
+ start: [0; 4],
+ structure_size: 0,
+ credit_charge: 0,
+ status: 0,
+ command: 0,
+ credits_requested: 0,
+ flags: 0,
+ next_command: 0,
+ message_id: 0,
+ async_id: 0,
+ session_id: 0,
+ security_signature: [0; 16],
+ payload: None,
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ SMB2HeaderState::Start => {
+ self.start[self.d.i] = *byte;
+ self.d.i += 1;
+ self.d
+ .next_state_when_i_reaches(SMB2HeaderState::StructureSize, 4);
+ }
+ SMB2HeaderState::StructureSize => {
+ self.structure_size =
+ self.d
+ .read_ule16(byte, self.structure_size, SMB2HeaderState::CreditsCharge)
+ }
+ SMB2HeaderState::CreditsCharge => {
+ self.credit_charge =
+ self.d
+ .read_ule16(byte, self.credit_charge, SMB2HeaderState::Status)
+ }
+ SMB2HeaderState::Status => {
+ self.status = self
+ .d
+ .read_ule32(byte, self.status, SMB2HeaderState::Command)
+ }
+ SMB2HeaderState::Command => {
+ self.command =
+ self.d
+ .read_ule16(byte, self.command, SMB2HeaderState::CreditsRequested)
+ }
+ SMB2HeaderState::CreditsRequested => {
+ self.credits_requested =
+ self.d
+ .read_ule16(byte, self.credits_requested, SMB2HeaderState::Flags)
+ }
+ SMB2HeaderState::Flags => {
+ self.flags = self
+ .d
+ .read_ule32(byte, self.flags, SMB2HeaderState::NextCommand)
+ }
+ SMB2HeaderState::NextCommand => {
+ self.next_command =
+ self.d
+ .read_ule32(byte, self.next_command, SMB2HeaderState::MessageId)
+ }
+ SMB2HeaderState::MessageId => {
+ self.message_id = self
+ .d
+ .read_ule64(byte, self.message_id, SMB2HeaderState::AsyncId)
+ }
+ SMB2HeaderState::AsyncId => {
+ self.async_id = self
+ .d
+ .read_ule64(byte, self.async_id, SMB2HeaderState::SessionId)
+ }
+ SMB2HeaderState::SessionId => {
+ self.session_id =
+ self.d
+ .read_ule64(byte, self.session_id, SMB2HeaderState::SecuritySignature)
+ }
+ SMB2HeaderState::SecuritySignature => {
+ self.security_signature[self.d.i] = *byte;
+ self.d.i += 1;
+ self.d.next_state_when_i_reaches(SMB2HeaderState::End, 16);
+ }
+ SMB2HeaderState::End => match self.get_payload() {
+ Some(pay) => pay.parse(byte),
+ None => return,
+ },
+ }
+ }
+
+ fn repl(
+ &self,
+ masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ let payload_resp = self.payload.as_ref()?.repl(masscanned, client_info, tcb)?;
+ let mut resp: Vec = Vec::new();
+ resp.extend_from_slice(b"\xfeSMB"); // Start
+ resp.extend_from_slice(&64_u16.to_le_bytes()); // StructureSize
+ resp.extend_from_slice(&0_u16.to_le_bytes()); // CreditCharge
+ resp.extend_from_slice(&0_u32.to_le_bytes()); // Status
+ resp.extend_from_slice(&self.command.to_le_bytes()); // Command
+ resp.extend_from_slice(&1_u16.to_le_bytes()); // CreditsRequested
+ resp.extend_from_slice(&1_u32.to_le_bytes()); // Flags = Response
+ resp.extend_from_slice(&0_u32.to_le_bytes()); // NextCommand
+ resp.extend_from_slice(&self.message_id.to_le_bytes()); // MessageId
+ resp.extend_from_slice(&self.async_id.to_le_bytes()); // AsyncId
+ resp.extend_from_slice(&self.session_id.to_le_bytes()); // SessionId
+ resp.extend_from_slice(&[0; 16]); // SecuritySignature
+ // Payload
+ resp.extend(payload_resp);
+ Some(resp)
+ }
+}
+
+impl SMB2Header {
+ fn get_payload(&mut self) -> Option<&mut SMB2Payload> {
+ if let Some(_) = &self.payload {
+ return self.payload.as_mut();
+ }
+ if self.flags & 1 == 1 {
+ // Response
+ return None;
+ }
+ self.payload = Some(match self.command {
+ 0x0000 => {
+ // Negotiate
+ SMB2Payload::NegotiateRequest(SMB2NegotiateRequest::new())
+ }
+ 0x0001 => {
+ // Setup
+ SMB2Payload::SessionSetupRequest(SMB2SessionSetupRequest::new())
+ }
+ _ => None?,
+ });
+ self.payload.as_mut()
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+enum SMB2NegotiateRequestState {
+ StructureSize,
+ DialectCount,
+ SecurityMode,
+ Reserved,
+ Capabilities,
+ ClientGUID,
+ NegotiateAndReserved2,
+ Dialects,
+ End,
+}
+
+#[derive(Debug, Clone)]
+struct SMB2NegotiateRequest {
+ // DISSECTION
+ d: PacketDissector,
+ _tmp_dialect: u16,
+ // STRUCT
+ structure_size: u16,
+ dialect_count: u16,
+ security_mode: u16,
+ capabilities: u32,
+ client_guid: [u8; 16],
+ dialects: HashSet,
+}
+const EPOCH_1601: u64 = 11644473600;
+
+impl MPacket for SMB2NegotiateRequest {
+ fn new() -> Self {
+ SMB2NegotiateRequest {
+ d: PacketDissector::new(SMB2NegotiateRequestState::StructureSize),
+ _tmp_dialect: 0,
+ structure_size: 0,
+ dialect_count: 0,
+ security_mode: 0,
+ capabilities: 0,
+ client_guid: [0; 16],
+ dialects: HashSet::new(),
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ SMB2NegotiateRequestState::StructureSize => {
+ self.structure_size = self.d.read_ule16(
+ byte,
+ self.structure_size,
+ SMB2NegotiateRequestState::DialectCount,
+ );
+ }
+ SMB2NegotiateRequestState::DialectCount => {
+ self.dialect_count = self.d.read_ule16(
+ byte,
+ self.dialect_count,
+ SMB2NegotiateRequestState::SecurityMode,
+ );
+ }
+ SMB2NegotiateRequestState::SecurityMode => {
+ self.security_mode = self.d.read_ule16(
+ byte,
+ self.security_mode,
+ SMB2NegotiateRequestState::Reserved,
+ );
+ }
+ SMB2NegotiateRequestState::Reserved => {
+ self.d.i += 1;
+ self.d
+ .next_state_when_i_reaches(SMB2NegotiateRequestState::Capabilities, 2);
+ }
+ SMB2NegotiateRequestState::Capabilities => {
+ self.capabilities = self.d.read_ule32(
+ byte,
+ self.capabilities,
+ SMB2NegotiateRequestState::ClientGUID,
+ );
+ }
+ SMB2NegotiateRequestState::ClientGUID => {
+ self.client_guid[self.d.i] = *byte;
+ self.d.i += 1;
+ self.d.next_state_when_i_reaches(
+ SMB2NegotiateRequestState::NegotiateAndReserved2,
+ 16,
+ );
+ }
+ SMB2NegotiateRequestState::NegotiateAndReserved2 => {
+ self.d.i += 1;
+ self.d
+ .next_state_when_i_reaches(SMB2NegotiateRequestState::Dialects, 8);
+ }
+ SMB2NegotiateRequestState::Dialects => {
+ self._tmp_dialect =
+ self.d
+ .read_ule16(byte, self._tmp_dialect, SMB2NegotiateRequestState::Dialects);
+ if self.d.i == 0 {
+ // Add to dialects list when finished
+ self.dialects.insert(self._tmp_dialect);
+ self._tmp_dialect = 0;
+ // Check if dialects list is finished
+ if self.dialects.len() == self.dialect_count as usize {
+ self.d.state = SMB2NegotiateRequestState::End;
+ }
+ }
+ }
+ SMB2NegotiateRequestState::End => {
+ return;
+ }
+ }
+ }
+ fn repl(
+ &self,
+ _masscanned: &Masscanned,
+ _client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ if !matches!(self.d.state, SMB2NegotiateRequestState::End) {
+ return None;
+ }
+ let mut resp: Vec = Vec::new();
+ let time: u64 = (EPOCH_1601
+ + SystemTime::now()
+ .duration_since(SystemTime::UNIX_EPOCH)
+ .unwrap()
+ .as_secs())
+ * (1e7 as u64);
+ // Chose dialect
+ let smb2_versions = [
+ (0x0202, "SMB 2.002"),
+ (0x0210, "SMB 2.1"),
+ (0x02ff, "SMB 2.???"),
+ (0x0300, "SMB 3.0"),
+ (0x0302, "SMB 2.0.2"),
+ (0x0310, "SMB 3.1.0"),
+ (0x0311, "SMB 3.1.1"),
+ ];
+ let mut dialect = None;
+ let mut dialect_name = "Unknown";
+ if let Some(smb_ver) = smb2_versions
+ .iter()
+ .find(|(d, _)| self.dialects.contains(d))
+ {
+ dialect = Some(smb_ver.0);
+ dialect_name = smb_ver.1;
+ }
+ resp.extend_from_slice(&0x41_u16.to_le_bytes()); // StructureSize
+ resp.extend_from_slice(&0x1_u16.to_le_bytes()); // SecurityMode
+ resp.extend_from_slice(&dialect?.to_le_bytes()); // DialectRevision
+ resp.extend_from_slice(&0x1_u16.to_le_bytes()); // NegotiateCount
+ resp.extend_from_slice(&self.client_guid); // GUID
+ resp.extend_from_slice(&0x1_u32.to_le_bytes()); // Capabilities
+ resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxTransactionSize
+ resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxReadSize
+ resp.extend_from_slice(&0x10000_u32.to_le_bytes()); // MaxWriteSize
+ resp.extend_from_slice(&time.to_le_bytes()); // ServerTime
+ resp.extend_from_slice(&time.to_le_bytes()); // ServerStartTime
+ resp.extend_from_slice(&0x80_u16.to_le_bytes()); // SecurityBloboffset
+ resp.extend_from_slice(&(SECURITY_BLOB_NEG_PROTO.len() as u16).to_le_bytes()); // SecurityBlobLength
+ resp.extend_from_slice(&0x0_u32.to_le_bytes()); // NegotiateContextOffset
+ resp.extend_from_slice(SECURITY_BLOB_NEG_PROTO); // SecurityBlob
+ warn!("SMB2 Negotiate-Protocol-Reply ({})", dialect_name);
+ Some(resp)
+ }
+}
+
+#[derive(Debug, Clone, Copy)]
+enum SMB2SetupRequestState {
+ StructureSize,
+ Flags,
+ SecurityMode,
+ Capabilities,
+ Channel,
+ SecurityBufferOffset,
+ SecurityLen,
+ PreviousSessionId,
+ SecurityBlob,
+ End,
+}
+
+#[derive(Debug, Clone)]
+struct SMB2SessionSetupRequest {
+ // DISSECTION
+ d: PacketDissector,
+ // STRUCT
+ structure_size: u16,
+ flags: u8,
+ security_mode: u8,
+ capabilities: u32,
+ channel: u32,
+ security_buffer_offset: u16,
+ security_len: u16,
+ previous_session_id: u64,
+}
+impl MPacket for SMB2SessionSetupRequest {
+ fn new() -> Self {
+ SMB2SessionSetupRequest {
+ d: PacketDissector::new(SMB2SetupRequestState::StructureSize),
+ structure_size: 0,
+ flags: 0,
+ security_mode: 0,
+ capabilities: 0,
+ channel: 0,
+ security_buffer_offset: 0,
+ security_len: 0,
+ previous_session_id: 0,
+ }
+ }
+
+ fn parse(&mut self, byte: &u8) {
+ match self.d.state {
+ SMB2SetupRequestState::StructureSize => {
+ self.structure_size =
+ self.d
+ .read_ule16(byte, self.structure_size, SMB2SetupRequestState::Flags);
+ }
+ SMB2SetupRequestState::Flags => {
+ self.flags = *byte;
+ self.d.next_state(SMB2SetupRequestState::SecurityMode);
+ }
+ SMB2SetupRequestState::SecurityMode => {
+ self.security_mode = *byte;
+ self.d.next_state(SMB2SetupRequestState::Capabilities);
+ }
+ SMB2SetupRequestState::Capabilities => {
+ self.capabilities =
+ self.d
+ .read_ule32(byte, self.capabilities, SMB2SetupRequestState::Channel);
+ }
+ SMB2SetupRequestState::Channel => {
+ self.channel = self.d.read_ule32(
+ byte,
+ self.channel,
+ SMB2SetupRequestState::SecurityBufferOffset,
+ );
+ }
+ SMB2SetupRequestState::SecurityBufferOffset => {
+ self.security_buffer_offset = self.d.read_ule16(
+ byte,
+ self.security_buffer_offset,
+ SMB2SetupRequestState::SecurityLen,
+ );
+ }
+ SMB2SetupRequestState::SecurityLen => {
+ self.security_len = self.d.read_ule16(
+ byte,
+ self.security_len,
+ SMB2SetupRequestState::PreviousSessionId,
+ );
+ }
+ SMB2SetupRequestState::PreviousSessionId => {
+ self.previous_session_id = self.d.read_ule64(
+ byte,
+ self.previous_session_id,
+ SMB2SetupRequestState::SecurityBlob,
+ );
+ }
+ SMB2SetupRequestState::SecurityBlob => {
+ self.d.i += 1;
+ self.d.next_state_when_i_reaches(
+ SMB2SetupRequestState::End,
+ self.security_len as usize,
+ );
+ }
+ SMB2SetupRequestState::End => {}
+ }
+ }
+
+ fn repl(
+ &self,
+ _masscanned: &Masscanned,
+ _client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ if !matches!(self.d.state, SMB2SetupRequestState::End) {
+ return None;
+ }
+ let mut resp: Vec = Vec::new();
+ resp.extend_from_slice(&0x9_u16.to_le_bytes()); // StructureSize
+ resp.extend_from_slice(&0x0_u16.to_le_bytes()); // SessionFlags
+ resp.extend_from_slice(&0x48_u16.to_le_bytes()); // SecurityBufferOffset
+ resp.extend_from_slice(&(SECURITY_BLOB_CHALLENGE.len() as u16).to_le_bytes()); // SecurityLen
+ resp.extend_from_slice(SECURITY_BLOB_CHALLENGE); // SecurityBlob
+ warn!("SMB2 SessionSetup-Reply");
+ Some(resp)
+ }
+}
+
+#[derive(Debug, Clone)]
+enum SMB2Payload {
+ NegotiateRequest(SMB2NegotiateRequest),
+ SessionSetupRequest(SMB2SessionSetupRequest),
+}
+
+impl SMB2Payload {
+ fn repl(
+ &self,
+ masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ tcb: Option<&mut TCPControlBlock>,
+ ) -> Option> {
+ match self {
+ SMB2Payload::NegotiateRequest(x) => x.repl(masscanned, client_info, tcb),
+ SMB2Payload::SessionSetupRequest(x) => x.repl(masscanned, client_info, tcb),
+ }
+ }
+ fn parse(&mut self, byte: &u8) {
+ match self {
+ SMB2Payload::NegotiateRequest(x) => x.parse(byte),
+ SMB2Payload::SessionSetupRequest(x) => x.parse(byte),
+ }
+ }
+}
+
+//////////////
+// Handlers //
+//////////////
+
+pub fn repl_smb1<'a>(
+ data: &'a [u8],
+ masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ tcb: Option<&mut TCPControlBlock>,
+) -> Option> {
+ let mut nbtsession: NBTSession = NBTSession::new();
+ for byte in data {
+ nbtsession.parse(byte);
+ }
+ nbtsession.repl(masscanned, client_info, tcb)
+}
+
+pub fn repl_smb2<'a>(
+ data: &'a [u8],
+ masscanned: &Masscanned,
+ client_info: &ClientInfo,
+ tcb: Option<&mut TCPControlBlock>,
+) -> Option> {
+ let mut nbtsession: NBTSession = NBTSession::new();
+ for byte in data {
+ nbtsession.parse(byte);
+ }
+ nbtsession.repl(masscanned, client_info, tcb)
+}
+
+///////////
+// Tests //
+///////////
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::logger::MetaLogger;
+
+ use itertools::assert_equal;
+ use pnet::util::MacAddr;
+ use std::str::FromStr;
+
+ // Sent by `smbclient -U "" -N -L 10.1.1.1 -d10 --option='client min protocol=NT1'`
+ const SMB1_REQ_NEGOTIATE: &[u8] = b"\x00\x00\x00T\xffSMBr\x00\x00\x00\x00\x18C\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xfe\xff\x00\x00\x00\x00\x001\x00\x02NT LANMAN 1.0\x00\x02NT LM 0.12\x00\x02SMB 2.002\x00\x02SMB 2.???\x00";
+ const SMB1_REQ_SESSION_SETUP: &[u8] = b"\x00\x00\x00\x9c\xffSMBs\x00\x00\x00\x00\x18C\xc8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x89T\x00\x00\x01\x00\x0c\xff\x00\x00\x00\xff\xff\x02\x00\x01\x00\x00\x00\x00\x00J\x00\x00\x00\x00\x00T\xc0\x00\x80a\x00`H\x06\x06+\x06\x01\x05\x05\x02\xa0>0<\xa0\x0e0\x0c\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2*\x04(NTLMSSP\x00\x01\x00\x00\x00\x15\x82\x08b\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x06\x01\x00\x00\x00\x00\x00\x0f\x00U\x00n\x00i\x00x\x00\x00\x00S\x00a\x00m\x00b\x00a\x00\x00\x00";
+ // Sent by `smbclient -U "" -N -L 10.1.1.1 -d10`
+ const SMB2_REQ_NEGOTIATE: &[u8] = b"\x00\x00\x00\xd0\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00$\x00\x08\x00\x01\x00\x00\x00\x7f\x00\x00\x00\rr3\x97\"c\x8fA\x9f\xe0\xbawQ\x87rbx\x00\x00\x00\x03\x00\x00\x00\x02\x02\x10\x02\"\x02$\x02\x00\x03\x02\x03\x10\x03\x11\x03\x00\x00\x00\x00\x01\x00&\x00\x00\x00\x00\x00\x01\x00 \x00\x01\x00\xd5Z\x89\x87>\x80\xcd\x02\xc2\xab\x08\xa3\xf4\x94\xb6A\x05\x11V\xeeE\x19p\x19\xed\x17v\xda\x9b\x08\x99V\x00\x00\x02\x00\x06\x00\x00\x00\x00\x00\x02\x00\x02\x00\x01\x00\x00\x00\x05\x00\x10\x00\x00\x00\x00\x001\x000\x00.\x001\x00.\x001\x00.\x001\x00";
+ const SMB2_REQ_SESSION_SETUP: &[u8] = b"\x00\x00\x00\xa2\xfeSMB@\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x19\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00X\x00J\x00\x00\x00\x00\x00\x00\x00\x00\x00`H\x06\x06+\x06\x01\x05\x05\x02\xa0>0<\xa0\x0e0\x0c\x06\n+\x06\x01\x04\x01\x827\x02\x02\n\xa2*\x04(NTLMSSP\x00\x01\x00\x00\x00\x15\x82\x08b\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x06\x01\x00\x00\x00\x00\x00\x0f";
+ // You can dissect any of those payloads with Scapy using NBTSession(b"...")
+
+ #[test]
+ fn test_smb1_protocol_nego_parsing() {
+ let mut nbtsession: NBTSession = NBTSession::new();
+ nbtsession.parse_all(SMB1_REQ_NEGOTIATE);
+ assert_eq!(nbtsession.nb_type, 0);
+ assert_eq!(nbtsession.length, 0x54);
+ let smb1 = nbtsession.payload.expect("Error while unpacking SMB");
+ assert_eq!(&smb1.start, b"\xffSMB");
+ assert_eq!(smb1.command, 0x72);
+ assert_eq!(smb1.status, 0);
+ assert_eq!(smb1.flags, 24);
+ assert_eq!(smb1.flags2, 51267);
+ assert_eq!(smb1.pid_high, 0);
+ assert_eq!(smb1.security_signature, [0; 8]);
+ assert_eq!(smb1.tid, 0);
+ assert_eq!(smb1.pid_low, 65534);
+ assert_eq!(smb1.uid, 0);
+ assert_eq!(smb1.mid, 0);
+ let neg_request = match smb1.payload.expect("Error while reading payload") {
+ SMB1Payload::NegotiateRequest(x) => x,
+ _ => panic!("Bad payload"),
+ };
+ assert_eq!(neg_request.word_count, 0);
+ assert_eq!(neg_request.byte_count, 49);
+ assert_equal(
+ neg_request.dialects,
+ Vec::from([
+ SMB1Dialect {
+ buffer_format: 2,
+ dialect_string: "NT LANMAN 1.0".to_string(),
+ },
+ SMB1Dialect {
+ buffer_format: 2,
+ dialect_string: "NT LM 0.12".to_string(),
+ },
+ SMB1Dialect {
+ buffer_format: 2,
+ dialect_string: "SMB 2.002".to_string(),
+ },
+ SMB1Dialect {
+ buffer_format: 2,
+ dialect_string: "SMB 2.???".to_string(),
+ },
+ ]),
+ );
+ }
+ #[test]
+ fn test_smb1_protocol_nego_reply() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let client_info = ClientInfo::new();
+ let answer = repl_smb1(SMB1_REQ_NEGOTIATE, &masscanned, &client_info, None)
+ .expect("Error: no answer");
+ let expected = [
+ 0, 0, 1, 149, 255, 83, 77, 66, 114, 0, 0, 0, 0, 152, 7, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 254, 255, 0, 0, 0, 0, 17, 1, 0, 3, 50, 0, 50, 0, 0, 0, 1, 0, 0, 0, 1, 0,
+ 0, 0, 0, 0, 252, 227, 1, 128, 0, 250, 218, 34, 238, 28, 216, 1, 60, 0, 0, 80, 1, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 130, 1, 60, 6, 6, 43, 6, 1, 5, 5, 2, 160,
+ 130, 1, 48, 48, 130, 1, 44, 160, 26, 48, 24, 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 30,
+ 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 10, 162, 130, 1, 12, 4, 130, 1, 8, 78, 69, 71,
+ 79, 69, 88, 84, 83, 1, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 112, 0, 0, 0, 49, 60, 42, 58,
+ 199, 43, 60, 169, 109, 172, 56, 116, 167, 221, 29, 91, 244, 82, 107, 23, 3, 138, 75,
+ 145, 194, 9, 125, 154, 143, 230, 44, 150, 92, 81, 36, 47, 144, 77, 71, 199, 173, 143,
+ 135, 107, 34, 2, 191, 198, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 92, 51, 83, 13, 234, 249, 13, 77, 178, 236, 74, 227, 120, 110, 195, 8, 78,
+ 69, 71, 79, 69, 88, 84, 83, 3, 0, 0, 0, 1, 0, 0, 0, 64, 0, 0, 0, 152, 0, 0, 0, 49, 60,
+ 42, 58, 199, 43, 60, 169, 109, 172, 56, 116, 167, 221, 29, 91, 92, 51, 83, 13, 234,
+ 249, 13, 77, 178, 236, 74, 227, 120, 110, 195, 8, 64, 0, 0, 0, 88, 0, 0, 0, 48, 86,
+ 160, 84, 48, 82, 48, 39, 128, 37, 48, 35, 49, 33, 48, 31, 6, 3, 85, 4, 3, 19, 24, 84,
+ 111, 107, 101, 110, 32, 83, 105, 103, 110, 105, 110, 103, 32, 80, 117, 98, 108, 105,
+ 99, 32, 75, 101, 121, 48, 39, 128, 37, 48, 35, 49, 33, 48, 31, 6, 3, 85, 4, 3, 19, 24,
+ 84, 111, 107, 101, 110, 32, 83, 105, 103, 110, 105, 110, 103, 32, 80, 117, 98, 108,
+ 105, 99, 32, 75, 101, 121,
+ ];
+ assert_eq!(answer[..0x3c], expected[..0x3c]); // Test equality except "ServerTime" field
+ assert_eq!(answer[0x3c + 8..], expected[0x3c + 8..]);
+ }
+ #[test]
+ fn test_smb1_session_setup_request_parse() {
+ let mut nbtsession: NBTSession = NBTSession::new();
+ nbtsession.parse_all(SMB1_REQ_SESSION_SETUP);
+ assert_eq!(nbtsession.nb_type, 0);
+ assert_eq!(nbtsession.length, 0x9c);
+ let smb1 = nbtsession.payload.expect("Error while unpacking SMB");
+ assert_eq!(&smb1.start, b"\xffSMB");
+ assert_eq!(smb1.command, 0x73);
+ assert_eq!(smb1.status, 0);
+ assert_eq!(smb1.flags, 24);
+ assert_eq!(smb1.flags2, 0xc843);
+ assert_eq!(smb1.pid_high, 0);
+ assert_eq!(smb1.tid, 0);
+ assert_eq!(smb1.pid_low, 21641);
+ assert_eq!(smb1.uid, 0);
+ assert_eq!(smb1.mid, 1);
+ let sess_setup_req = match smb1.payload.expect("Error while reading payload") {
+ SMB1Payload::SessionSetupRequest(x) => x,
+ _ => panic!("Bad type"),
+ };
+ assert_eq!(sess_setup_req.word_count, 12);
+ assert_eq!(sess_setup_req.and_x_command, 0xff);
+ assert_eq!(sess_setup_req.and_x_offset, 0);
+ assert_eq!(sess_setup_req.max_buffer_size, 0xffff);
+ assert_eq!(sess_setup_req.max_mpx_count, 2);
+ assert_eq!(sess_setup_req.vc_number, 1);
+ assert_eq!(sess_setup_req.session_key, 0);
+ assert_eq!(sess_setup_req.security_len, 74);
+ assert_eq!(sess_setup_req.server_capabilities, 0x8000c054);
+ assert_eq!(sess_setup_req.server_capabilities, 0x8000c054);
+ }
+ #[test]
+ fn test_smb1_session_setup_request_reply() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let client_info = ClientInfo::new();
+ let answer = repl_smb1(SMB1_REQ_SESSION_SETUP, &masscanned, &client_info, None)
+ .expect("Error: no answer");
+ let expected = [
+ 0, 0, 0, 250, 255, 83, 77, 66, 115, 0, 0, 0, 0, 152, 7, 200, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 137, 84, 0, 0, 1, 0, 4, 255, 0, 68, 0, 0, 0, 159, 0, 207, 0, 161, 129,
+ 156, 48, 129, 153, 160, 3, 10, 1, 1, 161, 12, 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 10,
+ 162, 129, 131, 4, 129, 128, 78, 84, 76, 77, 83, 83, 80, 0, 2, 0, 0, 0, 8, 0, 8, 0, 56,
+ 0, 0, 0, 21, 130, 138, 226, 36, 145, 168, 246, 243, 137, 45, 52, 0, 0, 0, 0, 0, 0, 0,
+ 0, 64, 0, 64, 0, 64, 0, 0, 0, 10, 0, 97, 74, 0, 0, 0, 15, 87, 0, 73, 0, 78, 0, 49, 0,
+ 2, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 1, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 4, 0,
+ 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 3, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 7, 0, 8, 0,
+ 255, 38, 57, 245, 66, 29, 216, 1, 0, 0, 0, 0, 87, 0, 105, 0, 110, 0, 100, 0, 111, 0,
+ 119, 0, 115, 0, 32, 0, 52, 0, 46, 0, 48, 0, 0, 0, 87, 0, 105, 0, 110, 0, 100, 0, 111,
+ 0, 119, 0, 115, 0, 32, 0, 52, 0, 46, 0, 48, 0, 0, 0,
+ ];
+ assert_eq!(answer, expected);
+ }
+ #[test]
+ fn test_smb2_protocol_nego_parsing() {
+ let mut nbtsession: NBTSession = NBTSession::new();
+ nbtsession.parse_all(SMB2_REQ_NEGOTIATE);
+ assert_eq!(nbtsession.nb_type, 0);
+ assert_eq!(nbtsession.length, 0xd0);
+ let smb2 = nbtsession.payload.expect("No SMB2 payload found !");
+ assert_eq!(&smb2.start, b"\xfeSMB");
+ assert_eq!(smb2.structure_size, 64);
+ assert_eq!(smb2.credit_charge, 0);
+ assert_eq!(smb2.status, 0);
+ assert_eq!(smb2.command, 0);
+ assert_eq!(smb2.credits_requested, 31);
+ assert_eq!(smb2.flags, 0);
+ assert_eq!(smb2.next_command, 0);
+ assert_eq!(smb2.message_id, 0);
+ assert_eq!(smb2.async_id, 0);
+ assert_eq!(smb2.session_id, 0);
+ assert_eq!(smb2.security_signature, [0; 16]);
+ let neg_request = match smb2.payload.expect("Error while reading payload") {
+ SMB2Payload::NegotiateRequest(x) => x,
+ _ => panic!("Invalid payload type"),
+ };
+ assert_eq!(neg_request.structure_size, 36);
+ assert_eq!(neg_request.dialect_count, 8);
+ assert_eq!(neg_request.security_mode, 1);
+ assert_eq!(neg_request.capabilities, 127);
+ assert_eq!(
+ neg_request.client_guid,
+ [13, 114, 51, 151, 34, 99, 143, 65, 159, 224, 186, 119, 81, 135, 114, 98]
+ );
+ assert_eq!(
+ neg_request.dialects,
+ HashSet::from([514, 528, 546, 548, 768, 770, 784, 785])
+ );
+ }
+ #[test]
+ fn test_smb2_protocol_nego_reply() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let client_info = ClientInfo::new();
+ let answer = repl_smb2(SMB2_REQ_NEGOTIATE, &masscanned, &client_info, None)
+ .expect("Error: no answer");
+ let expected = [
+ 0, 0, 1, 192, 254, 83, 77, 66, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65, 0, 1, 0, 2, 2, 1, 0, 13, 114, 51, 151, 34,
+ 99, 143, 65, 159, 224, 186, 119, 81, 135, 114, 98, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0,
+ 0, 0, 1, 0, 0, 103, 222, 3, 242, 28, 216, 1, 0, 103, 222, 3, 242, 28, 216, 1, 128, 0,
+ 64, 1, 0, 0, 0, 0, 96, 130, 1, 60, 6, 6, 43, 6, 1, 5, 5, 2, 160, 130, 1, 48, 48, 130,
+ 1, 44, 160, 26, 48, 24, 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 30, 6, 10, 43, 6, 1, 4,
+ 1, 130, 55, 2, 2, 10, 162, 130, 1, 12, 4, 130, 1, 8, 78, 69, 71, 79, 69, 88, 84, 83, 1,
+ 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 112, 0, 0, 0, 49, 60, 42, 58, 199, 43, 60, 169, 109,
+ 172, 56, 116, 167, 221, 29, 91, 244, 82, 107, 23, 3, 138, 75, 145, 194, 9, 125, 154,
+ 143, 230, 44, 150, 92, 81, 36, 47, 144, 77, 71, 199, 173, 143, 135, 107, 34, 2, 191,
+ 198, 0, 0, 0, 0, 0, 0, 0, 0, 96, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 92, 51,
+ 83, 13, 234, 249, 13, 77, 178, 236, 74, 227, 120, 110, 195, 8, 78, 69, 71, 79, 69, 88,
+ 84, 83, 3, 0, 0, 0, 1, 0, 0, 0, 64, 0, 0, 0, 152, 0, 0, 0, 49, 60, 42, 58, 199, 43, 60,
+ 169, 109, 172, 56, 116, 167, 221, 29, 91, 92, 51, 83, 13, 234, 249, 13, 77, 178, 236,
+ 74, 227, 120, 110, 195, 8, 64, 0, 0, 0, 88, 0, 0, 0, 48, 86, 160, 84, 48, 82, 48, 39,
+ 128, 37, 48, 35, 49, 33, 48, 31, 6, 3, 85, 4, 3, 19, 24, 84, 111, 107, 101, 110, 32,
+ 83, 105, 103, 110, 105, 110, 103, 32, 80, 117, 98, 108, 105, 99, 32, 75, 101, 121, 48,
+ 39, 128, 37, 48, 35, 49, 33, 48, 31, 6, 3, 85, 4, 3, 19, 24, 84, 111, 107, 101, 110,
+ 32, 83, 105, 103, 110, 105, 110, 103, 32, 80, 117, 98, 108, 105, 99, 32, 75, 101, 121,
+ ];
+ assert_eq!(answer[..0x6c], expected[..0x6c]); // Test equality except the 2 "ServerTime" fields
+ assert_eq!(answer[0x6c + 16..], expected[0x6c + 16..]);
+ }
+ #[test]
+ fn test_smb2_session_setup_request_parse() {
+ let mut nbtsession: NBTSession = NBTSession::new();
+ nbtsession.parse_all(SMB2_REQ_SESSION_SETUP);
+ assert_eq!(nbtsession.nb_type, 0);
+ assert_eq!(nbtsession.length, 0xa2);
+ let smb2 = nbtsession.payload.expect("Error while unpacking SMB");
+ assert_eq!(&smb2.start, b"\xfeSMB");
+ assert_eq!(smb2.command, 1);
+ assert_eq!(smb2.status, 0);
+ assert_eq!(smb2.flags, 0);
+ let sess_setup_req = match smb2.payload.expect("Error while reading payload") {
+ SMB2Payload::SessionSetupRequest(x) => x,
+ _ => panic!("Bad type"),
+ };
+ assert_eq!(sess_setup_req.structure_size, 0x19);
+ assert_eq!(sess_setup_req.flags, 0);
+ assert_eq!(sess_setup_req.security_mode, 1);
+ assert_eq!(sess_setup_req.capabilities, 1);
+ assert_eq!(sess_setup_req.channel, 0);
+ assert_eq!(sess_setup_req.security_buffer_offset, 0x58);
+ assert_eq!(sess_setup_req.security_len, 74);
+ assert_eq!(sess_setup_req.previous_session_id, 0);
+ }
+ #[test]
+ fn test_smb2_session_setup_request_reply() {
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:00:00:00:00:00").expect("error parsing default MAC address"),
+ iface: None,
+ self_ip_list: None,
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let client_info = ClientInfo::new();
+ let answer = repl_smb2(SMB2_REQ_SESSION_SETUP, &masscanned, &client_info, None)
+ .expect("Error: no answer");
+ let expected = [
+ 0, 0, 0, 231, 254, 83, 77, 66, 64, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0,
+ 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 72, 0, 159, 0, 161, 129, 156, 48,
+ 129, 153, 160, 3, 10, 1, 1, 161, 12, 6, 10, 43, 6, 1, 4, 1, 130, 55, 2, 2, 10, 162,
+ 129, 131, 4, 129, 128, 78, 84, 76, 77, 83, 83, 80, 0, 2, 0, 0, 0, 8, 0, 8, 0, 56, 0, 0,
+ 0, 21, 130, 138, 226, 36, 145, 168, 246, 243, 137, 45, 52, 0, 0, 0, 0, 0, 0, 0, 0, 64,
+ 0, 64, 0, 64, 0, 0, 0, 10, 0, 97, 74, 0, 0, 0, 15, 87, 0, 73, 0, 78, 0, 49, 0, 2, 0, 8,
+ 0, 87, 0, 73, 0, 78, 0, 49, 0, 1, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 4, 0, 8, 0, 87,
+ 0, 73, 0, 78, 0, 49, 0, 3, 0, 8, 0, 87, 0, 73, 0, 78, 0, 49, 0, 7, 0, 8, 0, 255, 38,
+ 57, 245, 66, 29, 216, 1, 0, 0, 0, 0,
+ ];
+ assert_eq!(answer, expected);
+ }
+}
diff --git a/src/proto/ssh.rs b/src/proto/ssh.rs
index a14c6d9..82ecd1b 100644
--- a/src/proto/ssh.rs
+++ b/src/proto/ssh.rs
@@ -16,21 +16,635 @@
use log::*;
-use std::str;
-
use crate::client::ClientInfo;
+use crate::proto::TCPControlBlock;
+use crate::utils::byte2str;
use crate::Masscanned;
-pub const SSH_PATTERN_CLIENT_PROTOCOL: &[u8; 7] = b"SSH-2.0";
+pub const SSH_PATTERN_CLIENT_PROTOCOL_2: &[u8; 7] = b"SSH-2.0";
+pub const SSH_PATTERN_CLIENT_PROTOCOL_1: &[u8; 8] = b"SSH-1.99";
+
+const SSH_STATE_START: usize = 0;
+const SSH_STATE_S1: usize = 1;
+const SSH_STATE_S2: usize = 2;
+const SSH_STATE_H: usize = 3;
+const SSH_STATE_DASH: usize = 4;
+const SSH_STATE_VERSION: usize = 5;
+const SSH_STATE_SOFTWARE: usize = 6;
+const SSH_STATE_COMMENT: usize = 7;
+const SSH_STATE_EOB: usize = 8;
+const SSH_STATE_LF: usize = 9;
+
+const SSH_STATE_FAIL: usize = 0xFFFF;
+
+struct ProtocolState {
+ state: usize,
+ prev_state: usize,
+ ssh_version: Vec,
+ ssh_software: Vec,
+ ssh_comment: Vec,
+}
+
+impl ProtocolState {
+ fn new() -> Self {
+ ProtocolState {
+ state: SSH_STATE_START,
+ prev_state: SSH_STATE_START,
+ ssh_version: Vec::::new(),
+ ssh_software: Vec::::new(),
+ ssh_comment: Vec::::new(),
+ }
+ }
+}
+
+fn ssh_parse(pstate: &mut ProtocolState, data: &[u8]) {
+ /* RFC 4253:
+ *
+ * 4.2. Protocol Version Exchange
+ *
+ * When the connection has been established, both sides MUST send an
+ * identification string. This identification string MUST be
+ *
+ * SSH-protoversion-softwareversion SP comments CR LF
+ *
+ * Since the protocol being defined in this set of documents is version
+ * 2.0, the 'protoversion' MUST be "2.0". The 'comments' string is
+ * OPTIONAL. If the 'comments' string is included, a 'space' character
+ * (denoted above as SP, ASCII 32) MUST separate the 'softwareversion'
+ * and 'comments' strings. The identification MUST be terminated by a
+ * single Carriage Return (CR) and a single Line Feed (LF) character
+ * (ASCII 13 and 10, respectively). Implementers who wish to maintain
+ * compatibility with older, undocumented versions of this protocol may
+ * want to process the identification string without expecting the
+ * presence of the carriage return character for reasons described in
+ * Section 5 of this document. The null character MUST NOT be sent.
+ * The maximum length of the string is 255 characters, including the
+ * Carriage Return and Line Feed.
+ */
+ let mut i = 0;
+ while i < data.len() {
+ match pstate.state {
+ SSH_STATE_START => {
+ pstate.state = SSH_STATE_S1;
+ continue;
+ }
+ /* first bytes should be "SSH-" */
+ SSH_STATE_S1 | SSH_STATE_S2 | SSH_STATE_H | SSH_STATE_DASH => {
+ if data[i] != b"SSH-"[pstate.state - SSH_STATE_S1] {
+ pstate.state = SSH_STATE_FAIL;
+ } else {
+ pstate.state += 1;
+ }
+ }
+ /* expect LF after a CR was read */
+ SSH_STATE_LF => {
+ if data[i] == b'\n' {
+ pstate.state = SSH_STATE_EOB;
+ } else {
+ if pstate.prev_state == SSH_STATE_SOFTWARE {
+ /* when reading software, \r can be followed by something else than \n */
+ pstate.state = pstate.prev_state;
+ /* cancel the read of this char */
+ i -= 1;
+ /* add the previously read \r to the software string */
+ pstate.ssh_software.push(b'\r');
+ } else if pstate.prev_state == SSH_STATE_COMMENT {
+ /* when reading comment, \r can be followed by something else than \n */
+ pstate.state = pstate.prev_state;
+ /* cancel the read of this char */
+ i -= 1;
+ /* add the previously read \r to the software string */
+ pstate.ssh_comment.push(b'\r');
+ } else {
+ /* in some other cases, it fails */
+ pstate.state = SSH_STATE_FAIL;
+ }
+ }
+ }
+ SSH_STATE_VERSION => {
+ if data[i] == b'-' {
+ pstate.state = SSH_STATE_SOFTWARE;
+ } else if !data[i].is_ascii_digit() && data[i] != b'.' {
+ pstate.state = SSH_STATE_FAIL;
+ } else {
+ pstate.ssh_version.push(data[i]);
+ }
+ }
+ SSH_STATE_SOFTWARE => {
+ if data[i] == b'\r' {
+ /* look for LF in the next char */
+ pstate.prev_state = pstate.state;
+ pstate.state = SSH_STATE_LF;
+ } else if data[i] == b' ' {
+ pstate.state = SSH_STATE_COMMENT;
+ } else {
+ pstate.ssh_software.push(data[i]);
+ }
+ }
+ SSH_STATE_COMMENT => {
+ if data[i] == b'\r' {
+ /* look for LF in the next char */
+ pstate.prev_state = pstate.state;
+ pstate.state = SSH_STATE_LF;
+ } else {
+ pstate.ssh_comment.push(data[i]);
+ }
+ }
+ SSH_STATE_FAIL => {
+ return;
+ }
+ SSH_STATE_EOB => { /* so far, do not parse after banner */ }
+ _ => {}
+ };
+ i += 1;
+ }
+}
pub fn repl<'a>(
data: &'a [u8],
_masscanned: &Masscanned,
- mut _client_info: &mut ClientInfo,
+ mut _client_info: &ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
) -> Option> {
debug!("receiving SSH data");
+ let mut pstate = ProtocolState::new();
+ ssh_parse(&mut pstate, data);
+ if pstate.state != SSH_STATE_EOB {
+ debug!("data in not correctly formatted - not responding");
+ debug!("pstate: {}", pstate.state);
+ return None;
+ }
let repl_data = b"SSH-2.0-1\r\n".to_vec();
debug!("sending SSH answer");
- warn!("SSH server banner to {}", str::from_utf8(&data).unwrap().trim_end());
- return Some(repl_data);
+ warn!("SSH server banner to {}", byte2str(&pstate.ssh_software));
+ Some(repl_data)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /* Reconstruct client's banner from the parsed information */
+ fn ssh_banner(pstate: &ProtocolState) -> Vec {
+ let mut banner = b"SSH-".to_vec();
+ for b in &pstate.ssh_version {
+ banner.push(*b);
+ }
+ banner.push(b'-');
+ for b in &pstate.ssh_software {
+ banner.push(*b);
+ }
+ if pstate.ssh_comment.len() > 0 {
+ banner.push(b' ');
+ for b in &pstate.ssh_comment {
+ banner.push(*b);
+ }
+ }
+ banner.push(b'\r');
+ banner.push(b'\n');
+ banner
+ }
+
+ #[test]
+ fn ssh_2_banner_parse() {
+ /* all at once */
+ let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ /* byte by byte */
+ let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ for i in 0..test_banner.len() {
+ if i == 0 {
+ assert!(pstate.state == SSH_STATE_START);
+ } else if i > 0 && i < 4 {
+ assert!(pstate.state == SSH_STATE_S1 + i);
+ } else if i >= 4 && i < 8 {
+ assert!(pstate.state == SSH_STATE_VERSION);
+ } else if i >= 8 && i < 17 {
+ assert!(pstate.state == SSH_STATE_SOFTWARE);
+ } else if i >= 17 && i < test_banner.len() - 1 {
+ assert!(pstate.state == SSH_STATE_COMMENT);
+ } else {
+ assert!(pstate.state == SSH_STATE_LF);
+ }
+ ssh_parse(&mut pstate, &test_banner[i..i + 1]);
+ }
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ }
+
+ #[test]
+ fn ssh_1_banner_parse() {
+ /* all at once */
+ let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* byte by byte */
+ let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ for i in 0..test_banner.len() {
+ if i == 0 {
+ assert!(pstate.state == SSH_STATE_START);
+ } else if i > 0 && i < 4 {
+ assert!(pstate.state == SSH_STATE_S1 + i);
+ } else if i >= 4 && i < 9 {
+ assert!(pstate.state == SSH_STATE_VERSION);
+ } else if i >= 9 && i < 18 {
+ assert!(pstate.state == SSH_STATE_SOFTWARE);
+ } else if i >= 18 && i < test_banner.len() - 1 {
+ assert!(pstate.state == SSH_STATE_COMMENT);
+ } else {
+ assert!(pstate.state == SSH_STATE_LF);
+ }
+ ssh_parse(&mut pstate, &test_banner[i..i + 1]);
+ }
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ }
+
+ #[test]
+ fn ssh_2_banner_space() {
+ /* space in SSH */
+ let test_banner = b"S SH-2.0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* space in VERSION */
+ let test_banner = b"SSH-2. 0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* space in software */
+ let test_banner = b"SSH-2.0-SOFT WARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFT");
+ assert!(pstate.ssh_comment == b"WARE COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* space in comment */
+ let test_banner = b"SSH-2.0-SOFTWARE COM MENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COM MENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* double space */
+ let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b" COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ }
+
+ #[test]
+ fn ssh_1_banner_space() {
+ /* space in SSH */
+ let test_banner = b"S SH-1.99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* space in VERSION */
+ let test_banner = b"SSH-1. 99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* space in software */
+ let test_banner = b"SSH-1.99-SOFT WARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFT");
+ assert!(pstate.ssh_comment == b"WARE COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* space in comment */
+ let test_banner = b"SSH-1.99-SOFTWARE COM MENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COM MENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* double space */
+ let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b" COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ }
+
+ #[test]
+ fn ssh_2_banner_cr() {
+ /* CR in SSH */
+ let test_banner = b"S\rSH-2.0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* CR in VERSION */
+ let test_banner = b"SSH-2.\r0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* CR in SOFTWARE */
+ let test_banner = b"SSH-2.0-SOFT\rWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFT\rWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* CR in COMMENT */
+ let test_banner = b"SSH-2.0-SOFTWARE COM\rMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COM\rMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* CR at the end */
+ let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT\r");
+ assert!(ssh_banner(&pstate) == test_banner);
+ }
+
+ #[test]
+ fn ssh_1_banner_cr() {
+ /* CR in SSH */
+ let test_banner = b"S\rSH-1.99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* CR in VERSION */
+ let test_banner = b"SSH-1.\r99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* CR in SOFTWARE */
+ let test_banner = b"SSH-1.99-SOFT\rWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFT\rWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* CR in COMMENT */
+ let test_banner = b"SSH-1.99-SOFTWARE COM\rMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COM\rMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* CR at the end */
+ let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT\r");
+ assert!(ssh_banner(&pstate) == test_banner);
+ }
+
+ #[test]
+ fn ssh_2_banner_lf() {
+ /* LF in SSH */
+ let test_banner = b"S\nSH-2.0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* LF in VERSION */
+ let test_banner = b"SSH-2.\n0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* LF in SOFTWARE */
+ let test_banner = b"SSH-2.0-SOFT\nWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFT\nWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* LF in COMMENT */
+ let test_banner = b"SSH-2.0-SOFTWARE COM\nMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COM\nMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* LF at the end */
+ let test_banner = b"SSH-2.0-SOFTWARE COMMENT\n\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT\n");
+ assert!(ssh_banner(&pstate) == test_banner);
+ }
+
+ #[test]
+ fn ssh_1_banner_lf() {
+ /* LF in SSH */
+ let test_banner = b"S\nSH-1.99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* LF in VERSION */
+ let test_banner = b"SSH-1.\n99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* LF in SOFTWARE */
+ let test_banner = b"SSH-1.99-SOFT\nWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFT\nWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* LF in COMMENT */
+ let test_banner = b"SSH-1.99-SOFTWARE COM\nMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COM\nMENT");
+ assert!(ssh_banner(&pstate) == test_banner);
+ /* LF at the end */
+ let test_banner = b"SSH-1.99-SOFTWARE COMMENT\n\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT\n");
+ assert!(ssh_banner(&pstate) == test_banner);
+ }
+
+ #[test]
+ fn ssh_2_banner_crlf() {
+ /* CRLF in SSH */
+ let test_banner = b"S\r\nSH-2.0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* CRLF in VERSION */
+ let test_banner = b"SSH-2.\r\n0-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* CRLF in SOFTWARE */
+ let test_banner = b"SSH-2.0-SOFT\r\nWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFT");
+ assert!(pstate.ssh_comment == b"");
+ assert!(ssh_banner(&pstate) == b"SSH-2.0-SOFT\r\n");
+ /* CRLF in COMMENT */
+ let test_banner = b"SSH-2.0-SOFTWARE COM\r\nMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COM");
+ assert!(ssh_banner(&pstate) == b"SSH-2.0-SOFTWARE COM\r\n");
+ /* CRLF at the end */
+ let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"2.0");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ assert!(ssh_banner(&pstate) == b"SSH-2.0-SOFTWARE COMMENT\r\n");
+ }
+
+ #[test]
+ fn ssh_1_banner_crlf() {
+ /* CRLF in SSH */
+ let test_banner = b"S\r\nSH-1.99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* CRLF in VERSION */
+ let test_banner = b"SSH-1.\r\n99-SOFTWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_FAIL);
+ /* CRLF in SOFTWARE */
+ let test_banner = b"SSH-1.99-SOFT\r\nWARE COMMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFT");
+ assert!(pstate.ssh_comment == b"");
+ assert!(ssh_banner(&pstate) == b"SSH-1.99-SOFT\r\n");
+ /* CRLF in COMMENT */
+ let test_banner = b"SSH-1.99-SOFTWARE COM\r\nMENT\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COM");
+ assert!(ssh_banner(&pstate) == b"SSH-1.99-SOFTWARE COM\r\n");
+ /* CRLF at the end */
+ let test_banner = b"SSH-1.99-SOFTWARE COMMENT\r\n\r\n";
+ let mut pstate = ProtocolState::new();
+ assert!(pstate.state == SSH_STATE_START);
+ ssh_parse(&mut pstate, test_banner);
+ assert!(pstate.state == SSH_STATE_EOB);
+ assert!(pstate.ssh_version == b"1.99");
+ assert!(pstate.ssh_software == b"SOFTWARE");
+ assert!(pstate.ssh_comment == b"COMMENT");
+ assert!(ssh_banner(&pstate) == b"SSH-1.99-SOFTWARE COMMENT\r\n");
+ }
}
diff --git a/src/proto/stun.rs b/src/proto/stun.rs
index a5c7ac4..24b209c 100644
--- a/src/proto/stun.rs
+++ b/src/proto/stun.rs
@@ -24,6 +24,7 @@ use byteorder::{BigEndian, ByteOrder};
use std::io;
use crate::client::ClientInfo;
+use crate::proto::TCPControlBlock;
use crate::Masscanned;
/* RFC 5389: The magic cookie field MUST contain the fixed value 0x2112A442 in
@@ -257,107 +258,6 @@ impl Into> for &StunAttribute {
}
}
-/*
-struct StunPacket {
- class: u8,
- method: u16,
- length: u16,
- magic: u32,
- id: u128,
- data: Vec,
- attributes: Vec,
-}
-
-impl StunPacket {
- fn new(data: &[u8]) -> Result {
- if data.len() < 20 {
- return Err(io::Error::new(
- io::ErrorKind::InvalidInput,
- "not enough data",
- ));
- }
- let class: u8 = ((data[0] & 0x01) << 1) | ((data[1] & 0x10) >> 4);
- let method: u16 = (((data[0] & 0b00111110) << 7) as u16) | ((data[1] & 0b11101111) as u16);
- let length: u16 = BigEndian::read_u16(&data[2..4]);
- let magic: u32 = BigEndian::read_u32(&data[4..8]);
- let id: u128 = ((BigEndian::read_u64(&data[8..16]) as u128) << 32)
- | (BigEndian::read_u32(&data[16..20]) as u128);
- if data.len() < 20 + length as usize {
- return Err(io::Error::new(
- io::ErrorKind::InvalidInput,
- "not enough data",
- ));
- }
- let data: Vec = data[20..(20 + length) as usize].to_vec();
- let mut stun = StunPacket {
- class,
- method,
- length,
- magic,
- id,
- data,
- attributes: Vec::::new(),
- };
- stun.attributes = stun.get_attributes();
- Ok(stun)
- }
-
- fn empty() -> Self {
- StunPacket {
- class: 0,
- method: 0,
- length: 0,
- magic: 0,
- id: 0,
- data: Vec::new(),
- attributes: Vec::new(),
- }
- }
-
- fn get_attributes(&self) -> Vec {
- let mut i = 0;
- let mut attributes = Vec::::new();
- while i + 4 < self.data.len() {
- let attr = StunAttribute::from(self.data[i..].to_vec());
- i += 4 + attr.len() as usize;
- attributes.push(attr);
- }
- attributes
- }
-
- fn set_length(&mut self) {
- self.length = 0;
- for attr in &self.attributes {
- self.length += 4 + attr.len();
- }
- }
-}
-
-impl Into> for StunPacket {
- fn into(self) -> Vec {
- let mut v = Vec::::new();
- // first cocktail with class and method bits
- v.push(
- TryInto::::try_into((self.method >> 7) & 0b00111110).unwrap()
- | TryInto::::try_into((self.class & 0b10) >> 1).unwrap(),
- );
- // second cocktail with class and method bits
- v.push(
- TryInto::::try_into((self.method & 0b01110000) << 1).unwrap()
- | TryInto::::try_into((self.class & 0b01) << 4).unwrap()
- | TryInto::::try_into(self.method & 0b00001111).unwrap(),
- );
- v.append(&mut self.length.to_be_bytes().to_vec());
- v.append(&mut self.magic.to_be_bytes().to_vec());
- v.append(&mut self.id.to_be_bytes()[4..].to_vec());
- for attr in &self.attributes {
- v.append(&mut attr.into());
- }
- v
- }
-}
-*/
-
struct StunPacket {
class: u8,
method: u16,
@@ -451,67 +351,11 @@ impl Into> for StunPacket {
}
}
-/*
pub fn repl<'a>(
data: &'a [u8],
_masscanned: &Masscanned,
- client_info: ClientInfo,
-) -> Option> {
- debug!("receiving STUN data");
- let stun_req: StunPacket = if let Ok(s) = StunPacket::new(&data) {
- s
- } else {
- return None;
- };
- if stun_req.class != STUN_CLASS_REQUEST {
- info!(
- "STUN packet not handled (class unknown: 0b{:b})",
- stun_req.class
- );
- return None;
- }
- if stun_req.method != STUN_METHOD_BINDING {
- info!(
- "STUN packet not handled (method unknown: 0x{:03x})",
- stun_req.method
- );
- return None;
- }
- /*
- * To be compatible with RFC3489: ignore magic
- if stun_req.magic != STUN_MAGIC {
- info!(
- "STUN packet not handled (magic unknown: 0x{:04x})",
- stun_req.magic
- );
- return None;
- }
- */
- if client_info.ip.src == None {
- error!("STUN packet not handled (expected client ip address not found)");
- return None;
- }
- if client_info.port.src == None {
- error!("STUN packet not handled (expected client port address not found)");
- return None;
- }
- let mut stun_resp: StunPacket = StunPacket::empty();
- stun_resp.class = STUN_CLASS_SUCCESS_RESPONSE;
- stun_resp.method = STUN_METHOD_BINDING;
- stun_resp.id = stun_req.id;
- stun_resp.attributes = Vec::::new();
- stun_resp.attributes.push(StunAttribute::MappedAddress(
- StunMappedAddressAttribute::new(client_info.ip.src.unwrap(), client_info.port.src.unwrap()),
- ));
- stun_resp.set_length();
- return Some(stun_resp.into());
-}
-*/
-
-pub fn repl<'a>(
- data: &'a [u8],
- _masscanned: &Masscanned,
- mut client_info: &mut ClientInfo,
+ client_info: &mut ClientInfo,
+ _tcb: Option<&mut TCPControlBlock>,
) -> Option> {
debug!("receiving STUN data");
let stun_req: StunPacket = if let Ok(s) = StunPacket::new(&data) {
@@ -571,6 +415,8 @@ mod tests {
use pnet::util::MacAddr;
+ use crate::logger::MetaLogger;
+
#[test]
fn test_proto_stun_ipv4() {
/* test payload is:
@@ -596,9 +442,11 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
- let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) {
+ let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) {
r
} else {
panic!("expected an answer, got None");
@@ -655,13 +503,15 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
client_info.ip.src = Some(IpAddr::V6(test_ip_addr));
client_info.ip.dst = Some(IpAddr::V6(masscanned_ip_addr));
client_info.port.src = Some(55000);
client_info.port.dst = Some(65000);
- let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) {
+ let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) {
r
} else {
panic!("expected an answer, got None");
@@ -706,13 +556,15 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
client_info.ip.src = Some(IpAddr::V4(test_ip_addr));
client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr));
client_info.port.src = Some(55000);
client_info.port.dst = Some(65000);
- let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) {
+ let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) {
r
} else {
panic!("expected an answer, got None");
@@ -755,13 +607,15 @@ mod tests {
synack_key: [0, 0],
mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
iface: None,
- ip_addresses: Some(&ips),
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
};
client_info.ip.src = Some(IpAddr::V4(test_ip_addr));
client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr));
client_info.port.src = Some(55000);
client_info.port.dst = Some(65535);
- let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info) {
+ let payload_resp = if let Some(r) = repl(payload, &masscanned, &mut client_info, None) {
r
} else {
panic!("expected an answer, got None");
diff --git a/src/proto/tcb.rs b/src/proto/tcb.rs
new file mode 100644
index 0000000..0f2a815
--- /dev/null
+++ b/src/proto/tcb.rs
@@ -0,0 +1,270 @@
+// This file is part of masscanned.
+// Copyright 2021 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+use lazy_static::lazy_static;
+
+use std::collections::HashMap;
+use std::sync::Mutex;
+
+use super::http::ProtocolState as HTTPProtocolState;
+use super::rpc::ProtocolState as RPCProtocolState;
+use crate::proto::{BASE_STATE, PROTO_NONE};
+
+pub enum ProtocolState {
+ HTTP(HTTPProtocolState),
+ RPC(RPCProtocolState),
+}
+
+pub struct TCPControlBlock {
+ /* state used to detect protocols (not specific) */
+ pub smack_state: usize,
+ /* detected protocol */
+ pub proto_id: usize,
+ /* internal state of protocol parser (e.g., HTTP parsing) */
+ pub proto_state: Option,
+}
+
+lazy_static! {
+ static ref CONTABLE: Mutex> = Mutex::new(HashMap::new());
+}
+
+pub fn is_tcb_set(cookie: u32) -> bool {
+ CONTABLE.lock().unwrap().contains_key(&cookie)
+}
+
+pub fn get_tcb(cookie: u32, mut f: F)
+where
+ F: FnMut(Option<&mut TCPControlBlock>),
+{
+ f(CONTABLE.lock().unwrap().get_mut(&cookie));
+}
+
+pub fn add_tcb(cookie: u32) {
+ let mut ct = CONTABLE.lock().unwrap();
+ let tcb = TCPControlBlock {
+ smack_state: BASE_STATE,
+ proto_id: PROTO_NONE,
+ proto_state: None,
+ };
+ if !ct.contains_key(&cookie) {
+ ct.insert(cookie, tcb);
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::collections::HashSet;
+ use std::net::{IpAddr, Ipv4Addr};
+ use std::str::FromStr;
+
+ use pnet::{
+ packet::{ip::IpNextHeaderProtocols, tcp::TcpPacket},
+ util::MacAddr,
+ };
+
+ use crate::client::ClientInfo;
+ use crate::layer_4::tcp;
+ use crate::logger::MetaLogger;
+ use crate::proto::{PROTO_HTTP, PROTO_RPC_TCP};
+ use crate::synackcookie;
+ use crate::Masscanned;
+
+ fn get_dummy_tcp(&client_info: &ClientInfo) -> Vec {
+ /* Craft a TCP ACK+PUSH packet with correct ports and ack */
+ let mut pkt = Vec::new();
+ pkt.extend_from_slice(&client_info.port.src.unwrap().to_be_bytes());
+ pkt.extend_from_slice(&client_info.port.dst.unwrap().to_be_bytes());
+ pkt.extend_from_slice(b"\x00\x00\x00\x00");
+ pkt.extend_from_slice(&(client_info.cookie.unwrap() + 1).to_be_bytes());
+ pkt.extend_from_slice(b"P\x18 \x00\x00\x00\x00\x00");
+ pkt
+ }
+
+ #[test]
+ fn test_proto_tcb_proto_id() {
+ let mut client_info = ClientInfo::new();
+ let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0);
+ client_info.ip.src = Some(IpAddr::V4(test_ip_addr));
+ client_info.port.src = Some(65000);
+ client_info.port.dst = Some(80);
+ client_info.transport = Some(IpNextHeaderProtocols::Tcp);
+ let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3);
+ client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr));
+ let mut ips = HashSet::new();
+ ips.insert(IpAddr::V4(masscanned_ip_addr));
+ /* Construct masscanned context object */
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
+ iface: None,
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let cookie = synackcookie::generate(&client_info, &masscanned.synack_key).unwrap();
+ client_info.cookie = Some(cookie);
+ assert!(!is_tcb_set(cookie), "expected no TCB entry, found one");
+ /***** TEST PROTOCOL ID IN TCB *****/
+ let payload = [get_dummy_tcp(&client_info), b"GET / HTTP/1.1\r\n".to_vec()].concat();
+ tcp::repl(
+ &TcpPacket::new(&payload).unwrap(),
+ &masscanned,
+ &mut client_info,
+ );
+ assert!(is_tcb_set(cookie), "expected a TCB entry, not found");
+ get_tcb(cookie, |t| {
+ let t = t.unwrap();
+ assert!(t.proto_id == PROTO_HTTP);
+ });
+
+ /***** SENDING MORE DATA *****/
+ let payload = [
+ get_dummy_tcp(&client_info),
+ b"garbage data with no specific format (no protocol)\r\n\r\n".to_vec(),
+ ]
+ .concat();
+ tcp::repl(
+ &TcpPacket::new(&payload).unwrap(),
+ &masscanned,
+ &mut client_info,
+ );
+ assert!(is_tcb_set(cookie), "expected a TCB entry, not found");
+ get_tcb(cookie, |t| {
+ let t = t.unwrap();
+ assert!(t.proto_id == PROTO_HTTP);
+ });
+ }
+
+ #[test]
+ fn test_proto_tcb_proto_state_http() {
+ let mut client_info = ClientInfo::new();
+ let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0);
+ client_info.ip.src = Some(IpAddr::V4(test_ip_addr));
+ client_info.port.src = Some(65001);
+ client_info.port.dst = Some(80);
+ client_info.transport = Some(IpNextHeaderProtocols::Tcp);
+ let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3);
+ client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr));
+ let mut ips = HashSet::new();
+ ips.insert(IpAddr::V4(masscanned_ip_addr));
+ /* Construct masscanned context object */
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
+ iface: None,
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let cookie = synackcookie::generate(&client_info, &masscanned.synack_key).unwrap();
+ client_info.cookie = Some(cookie);
+ assert!(!is_tcb_set(cookie), "expected no TCB entry, found one");
+ /***** TEST PROTOCOL ID IN TCB *****/
+ let payload = [get_dummy_tcp(&client_info), b"GET / HTTP/1.1\r\n".to_vec()].concat();
+ tcp::repl(
+ &TcpPacket::new(&payload).unwrap(),
+ &masscanned,
+ &mut client_info,
+ );
+ assert!(is_tcb_set(cookie), "expected a TCB entry, not found");
+ get_tcb(cookie, |t| {
+ let t = t.unwrap();
+ assert!(t.proto_id == PROTO_HTTP);
+ if let Some(ProtocolState::HTTP(_)) = t.proto_state {
+ } else {
+ panic!("expected a HTTP protocole state, found None");
+ }
+ });
+ /***** SENDING MORE DATA *****/
+ let payload = [
+ get_dummy_tcp(&client_info),
+ b"Field: empty\r\n\r\n".to_vec(),
+ ]
+ .concat();
+ /* Should have an answer here */
+ if let None = tcp::repl(
+ &TcpPacket::new(&payload).unwrap(),
+ &masscanned,
+ &mut client_info,
+ ) {
+ panic!("expected an HTTP response, got nothing");
+ }
+ assert!(is_tcb_set(cookie), "expected a TCB entry, not found");
+ get_tcb(cookie, |t| {
+ let t = t.unwrap();
+ assert!(t.proto_id == PROTO_HTTP);
+ })
+ }
+
+ #[test]
+ fn test_proto_tcb_proto_state_rpc() {
+ let mut client_info = ClientInfo::new();
+ let test_ip_addr = Ipv4Addr::new(3, 2, 1, 0);
+ client_info.ip.src = Some(IpAddr::V4(test_ip_addr));
+ client_info.port.src = Some(65002);
+ client_info.port.dst = Some(80);
+ client_info.transport = Some(IpNextHeaderProtocols::Tcp);
+ let masscanned_ip_addr = Ipv4Addr::new(0, 1, 2, 3);
+ client_info.ip.dst = Some(IpAddr::V4(masscanned_ip_addr));
+ let mut ips = HashSet::new();
+ ips.insert(IpAddr::V4(masscanned_ip_addr));
+ /* Construct masscanned context object */
+ let masscanned = Masscanned {
+ synack_key: [0, 0],
+ mac: MacAddr::from_str("00:11:22:33:44:55").expect("error parsing MAC address"),
+ iface: None,
+ self_ip_list: Some(&ips),
+ remote_ip_deny_list: None,
+ log: MetaLogger::new(),
+ };
+ let cookie = synackcookie::generate(&client_info, &masscanned.synack_key).unwrap();
+ client_info.cookie = Some(cookie);
+ assert!(!is_tcb_set(cookie), "expected no TCB entry, found one");
+ /***** TEST PROTOCOL ID IN TCB *****/
+ let full_payload = b"\x80\x00\x00\x28\x72\xfe\x1d\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x01\x97\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";
+ let payload = [get_dummy_tcp(&client_info), full_payload[0..28].to_vec()].concat();
+ tcp::repl(
+ &TcpPacket::new(&payload).unwrap(),
+ &masscanned,
+ &mut client_info,
+ );
+ assert!(is_tcb_set(cookie), "expected a TCB entry, not found");
+ get_tcb(cookie, |t| {
+ let t = t.unwrap();
+ assert!(t.proto_id == PROTO_RPC_TCP);
+ if let Some(ProtocolState::RPC(_)) = t.proto_state {
+ } else {
+ panic!("expected a RPC protocole state, found None");
+ }
+ });
+ /***** SENDING MORE DATA *****/
+ /* Should have an answer here */
+ let payload = [get_dummy_tcp(&client_info), full_payload[28..].to_vec()].concat();
+ if let None = tcp::repl(
+ &TcpPacket::new(&payload).unwrap(),
+ &masscanned,
+ &mut client_info,
+ ) {
+ panic!("expected a RPC response, got nothing");
+ }
+ assert!(is_tcb_set(cookie), "expected a TCB entry, not found");
+ get_tcb(cookie, |t| {
+ let t = t.unwrap();
+ assert!(t.proto_id == PROTO_RPC_TCP);
+ });
+ }
+}
diff --git a/src/smack/smack.rs b/src/smack/smack.rs
index bb37358..8248ec1 100644
--- a/src/smack/smack.rs
+++ b/src/smack/smack.rs
@@ -61,7 +61,7 @@ pub struct Smack {
}
fn make_copy_of_pattern(pattern: &[u8], is_nocase: bool) -> Vec {
- let mut p = pattern.clone().to_vec();
+ let mut p = pattern.to_vec();
for i in 0..p.len() {
if is_nocase {
p[i] = p[i].to_ascii_lowercase();
diff --git a/src/smack/smack_utils.rs b/src/smack/smack_utils.rs
index 79bb459..bed3b6e 100644
--- a/src/smack/smack_utils.rs
+++ b/src/smack/smack_utils.rs
@@ -1,4 +1,5 @@
bitflags! {
+ #[derive(Clone, Copy)]
pub struct SmackFlags: usize {
const EMPTY = 0x00;
const ANCHOR_BEGIN = 0x01;
diff --git a/src/utils/display.rs b/src/utils/display.rs
new file mode 100644
index 0000000..45e3d8f
--- /dev/null
+++ b/src/utils/display.rs
@@ -0,0 +1,48 @@
+// This file is part of masscanned.
+// Copyright 2021 - 2022 - The IVRE project
+//
+// Masscanned is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Masscanned is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+// License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Masscanned. If not, see .
+
+static CHARS: [&'static str; 256] = [
+ "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\x07", "\\x08", "\\x09",
+ "\\x0a", "\\x0b", "\\x0c", "\\x0d", "\\x0e", "\\x0f", "\\x10", "\\x11", "\\x12", "\\x13",
+ "\\x14", "\\x15", "\\x16", "\\x17", "\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d",
+ "\\x1e", "\\x1f", " ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", "*", "+", ",", "-", ".",
+ "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A",
+ "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
+ "U", "V", "W", "X", "Y", "Z", "[", "\\", "]", "^", "_", "`", "a", "b", "c", "d", "e", "f", "g",
+ "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
+ "{", "|", "}", "~", "\\x7f", "\\x80", "\\x81", "\\x82", "\\x83", "\\x84", "\\x85", "\\x86",
+ "\\x87", "\\x88", "\\x89", "\\x8a", "\\x8b", "\\x8c", "\\x8d", "\\x8e", "\\x8f", "\\x90",
+ "\\x91", "\\x92", "\\x93", "\\x94", "\\x95", "\\x96", "\\x97", "\\x98", "\\x99", "\\x9a",
+ "\\x9b", "\\x9c", "\\x9d", "\\x9e", "\\x9f", "\\xa0", "\\xa1", "\\xa2", "\\xa3", "\\xa4",
+ "\\xa5", "\\xa6", "\\xa7", "\\xa8", "\\xa9", "\\xaa", "\\xab", "\\xac", "\\xad", "\\xae",
+ "\\xaf", "\\xb0", "\\xb1", "\\xb2", "\\xb3", "\\xb4", "\\xb5", "\\xb6", "\\xb7", "\\xb8",
+ "\\xb9", "\\xba", "\\xbb", "\\xbc", "\\xbd", "\\xbe", "\\xbf", "\\xc0", "\\xc1", "\\xc2",
+ "\\xc3", "\\xc4", "\\xc5", "\\xc6", "\\xc7", "\\xc8", "\\xc9", "\\xca", "\\xcb", "\\xcc",
+ "\\xcd", "\\xce", "\\xcf", "\\xd0", "\\xd1", "\\xd2", "\\xd3", "\\xd4", "\\xd5", "\\xd6",
+ "\\xd7", "\\xd8", "\\xd9", "\\xda", "\\xdb", "\\xdc", "\\xdd", "\\xde", "\\xdf", "\\xe0",
+ "\\xe1", "\\xe2", "\\xe3", "\\xe4", "\\xe5", "\\xe6", "\\xe7", "\\xe8", "\\xe9", "\\xea",
+ "\\xeb", "\\xec", "\\xed", "\\xee", "\\xef", "\\xf0", "\\xf1", "\\xf2", "\\xf3", "\\xf4",
+ "\\xf5", "\\xf6", "\\xf7", "\\xf8", "\\xf9", "\\xfa", "\\xfb", "\\xfc", "\\xfd", "\\xfe",
+ "\\xff",
+];
+
+pub fn byte2str(data: &[u8]) -> String {
+ let mut result = String::new();
+ for byte in data {
+ result.push_str(CHARS[usize::from(*byte)]);
+ }
+ return result;
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index 3c788a6..f25ca3d 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -1,3 +1,7 @@
mod parsers;
pub use parsers::IpAddrParser;
+
+mod display;
+
+pub use display::byte2str;
diff --git a/src/utils/parsers.rs b/src/utils/parsers.rs
index a17210d..e2a9ac7 100644
--- a/src/utils/parsers.rs
+++ b/src/utils/parsers.rs
@@ -5,12 +5,12 @@ use std::io::BufReader;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use log::*;
-use pcap_file::pcap::{Packet, PcapReader};
+use pcap_file::pcap::{PcapPacket, PcapReader};
use pnet::packet::{
ethernet::{EtherTypes, EthernetPacket},
ipv4::Ipv4Packet,
ipv6::Ipv6Packet,
- Packet as Pkt,
+ Packet,
};
/* Generic IP packet (either IPv4 or IPv6) */
@@ -134,9 +134,46 @@ impl IpAddrParser for File {
}
}
+/* Parse IP addresses from a comma-separated list in a string */
+impl IpAddrParser for &str {
+ fn extract_ip_addresses_with_count(
+ self,
+ _blacklist: Option>,
+ ) -> HashMap {
+ panic!("not implemented");
+ }
+
+ fn extract_ip_addresses_only(self, blacklist: Option>) -> HashSet {
+ let mut ip_addresses = HashSet::new();
+ for line in self.split(",") {
+ /* Should never occur */
+ if line.is_empty() {
+ warn!("cannot parse line: {}", line);
+ continue;
+ }
+ let ip: IpAddr;
+ if let Ok(val) = line.parse::() {
+ ip = IpAddr::V4(val);
+ } else if let Ok(val) = line.parse::() {
+ ip = IpAddr::V6(val);
+ } else {
+ warn!("cannot parse IP address from line: {}", line);
+ continue;
+ }
+ if let Some(ref b) = blacklist {
+ if b.contains(&ip) {
+ info!("[blacklist] ignoring {}", &ip);
+ continue;
+ }
+ }
+ ip_addresses.insert(ip);
+ }
+ ip_addresses
+ }
+}
/* Get the IP address of source and dest. from an IP packet.
* works with both IPv4 and IPv6 packets/addresses */
-fn extract_ip(pkt: Packet) -> Option<(IpAddr, IpAddr)> {
+fn extract_ip(pkt: PcapPacket) -> Option<(IpAddr, IpAddr)> {
let eth = EthernetPacket::new(&pkt.data).expect("error parsing Ethernet packet");
let payload = eth.payload();
let ip = match eth.get_ethertype() {
@@ -169,13 +206,13 @@ impl IpAddrParser for PcapReader {
/* Extract IP addresses (v4 and v6) from a capture and count occurrences of
* each. */
fn extract_ip_addresses_with_count(
- self: PcapReader,
+ mut self: PcapReader,
blacklist: Option>,
) -> HashMap {
let mut ip_addresses = HashMap::new();
// pcap.map(fn) , map_Ok
// .iter, into_iter
- for pkt in self {
+ while let Some(pkt) = self.next_packet() {
match pkt {
Ok(pkt) => {
// map_Some map_None
@@ -209,13 +246,13 @@ impl IpAddrParser for PcapReader {
ip_addresses
}
fn extract_ip_addresses_only(
- self: PcapReader,
+ mut self: PcapReader,
blacklist: Option>,
) -> HashSet {
let mut ip_addresses = HashSet::new();
// pcap.map(fn) , map_Ok
// .iter, into_iter
- for pkt in self {
+ while let Some(pkt) = self.next_packet() {
match pkt {
Ok(pkt) => {
// map_Some map_None
diff --git a/test/requirements.txt b/test/requirements.txt
new file mode 100644
index 0000000..fd8f3e8
--- /dev/null
+++ b/test/requirements.txt
@@ -0,0 +1,3 @@
+ivre
+scapy
+requests
diff --git a/test/src/all.py b/test/src/all.py
index e031228..561993e 100644
--- a/test/src/all.py
+++ b/test/src/all.py
@@ -14,580 +14,31 @@
# You should have received a copy of the GNU General Public License
# along with Masscanned. If not, see .
-from scapy.all import *
-import requests
-import requests.packages.urllib3.util.connection as urllib3_cn
-import logging
+import importlib
+import os
-from .conf import *
+# Export / other tests
+from .core import test_all # noqa: F401
-fmt = logging.Formatter("%(levelname)s\t%(message)s")
-ch = logging.StreamHandler()
-ch.setFormatter(fmt)
-ch.setLevel(logging.DEBUG)
-LOG = logging.getLogger(__name__)
-LOG.setLevel(logging.DEBUG)
-LOG.addHandler(ch)
+DEFAULT_TESTS = [
+ "arp",
+ "dns",
+ "ghost",
+ "http",
+ "icmpv4",
+ "icmpv6",
+ "ip",
+ "rpc",
+ "smb",
+ "ssh",
+ "stun",
+ "tcp",
+ "udp",
+]
-tests = list()
+ENABLED_TESTS = DEFAULT_TESTS
+if tests := os.environ.get("TESTS"):
+ ENABLED_TESTS = [x.strip() for x in tests.split(",")]
-# decorator to automatically add a function to tests
-def test(f):
- OK = "\033[1mOK\033[0m"
- KO = "\033[1m\033[1;%dmKO\033[0m" % 31
- global tests
- fname = f.__name__.ljust(50, '.')
- def w(iface):
- try:
- f(iface)
- LOG.info("{}{}".format(fname, OK))
- except AssertionError as e:
- LOG.info("{}{}: {}".format(fname, KO, e))
- tests.append(w)
- return w
-
-def multicast(ip6):
- a, b = ip6.split(":")[-2:]
- mac = ["33", "33", "ff"]
- if len(a) == 4:
- mac.append(a[2:])
- else:
- mac.append("00")
- if len(b) >= 2:
- mac.append(b[:2])
- else:
- mac.append("00")
- if len(b) >= 4:
- mac.append(b[2:])
- else:
- mac.append("00")
- return ":".join(mac)
-
-def check_ip_checksum(pkt):
- assert(IP in pkt), "no IP layer found"
- ip_pkt = pkt[IP]
- chksum = ip_pkt.chksum
- del ip_pkt.chksum
- assert(IP(raw(ip_pkt)).chksum == chksum), "bad IPv4 checksum"
-
-def check_ipv6_checksum(pkt):
- assert(IPv6 in pkt), "no IP layer found"
- ip_pkt = pkt[IPv6]
- chksum = ip_pkt.chksum
- del ip_pkt.chksum
- assert(IPv6(raw(ip_pkt)).chksum == chksum), "bad IPv6 checksum"
-
-@test
-def test_arp_req(iface):
- ##### ARP #####
- arp_req = Ether()/ARP(psrc='192.0.0.2', pdst=IPV4_ADDR)
- arp_repl = iface.sr1(arp_req, timeout=1)
- assert(arp_repl is not None), "expecting answer, got nothing"
- assert(ARP in arp_repl), "no ARP layer found"
- arp_repl = arp_repl[ARP]
- # check answer
- ## op is "is-at"
- assert(arp_repl.op == 2), "unexpected ARP op: {}".format(arp_repl.op)
- ## answer for the requested IP
- assert(arp_repl.psrc == arp_req.pdst), "unexpected ARP psrc: {}".format(arp_repl.psrc)
- assert(arp_repl.pdst == arp_req.psrc), "unexpected ARP pdst: {}".format(arp_repl.pdst)
- ## answer is expected MAC address
- assert(arp_repl.hwsrc == MAC_ADDR), "unexpected ARP hwsrc: {}".format(arp_repl.hwsrc)
-
-@test
-def test_arp_req_other_ip(iface):
- ##### ARP #####
- arp_req = Ether()/ARP(psrc='192.0.0.2', pdst='1.2.3.4')
- arp_repl = iface.sr1(arp_req, timeout=1)
- assert(arp_repl is None), "responding to ARP requests for other IP addresses"
-
-@test
-def test_ipv4_req(iface):
- ##### IP #####
- ip_req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR, id=0x1337)/ICMP(type=8, code=0)
- ip_repl = iface.sr1(ip_req, timeout=1)
- assert(ip_repl is not None), "expecting answer, got nothing"
- check_ip_checksum(ip_repl)
- assert(IP in ip_repl), "no IP layer in response"
- ip_repl = ip_repl[IP]
- assert(ip_repl.id == 0), "IP identification unexpected"
-
-@test
-def test_eth_req_other_mac(iface):
- #### ETH ####
- ip_req = Ether(dst="00:00:00:11:11:11")/IP(dst=IPV4_ADDR)/ICMP(type=8, code=0)
- ip_repl = iface.sr1(ip_req, timeout=1)
- assert(ip_repl is None), "responding to other MAC addresses"
-
-@test
-def test_ipv4_req_other_ip(iface):
- ##### IP #####
- ip_req = Ether(dst=MAC_ADDR)/IP(dst="1.2.3.4")/ICMP(type=8, code=0)
- ip_repl = iface.sr1(ip_req, timeout=1)
- assert(ip_repl is None), "responding to other IP addresses"
-
-@test
-def test_icmpv4_echo_req(iface):
- ##### ICMPv4 #####
- icmp_req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/ICMP(type=8, code=0)/Raw("idrinkwaytoomuchcoffee")
- icmp_repl = iface.sr1(icmp_req, timeout=1)
- assert(icmp_repl is not None), "expecting answer, got nothing"
- check_ip_checksum(icmp_repl)
- assert(ICMP in icmp_repl)
- icmp_repl = icmp_repl[ICMP]
- # check answer
- ## type is "echo-reply"
- assert(icmp_repl.type == 0)
- assert(icmp_repl.code == 0)
- ## data is the same as sent
- assert(icmp_repl.load == icmp_req.load)
-
-@test
-def test_icmpv6_neighbor_solicitation(iface):
- ##### IPv6 Neighbor Solicitation #####
- for mac in ["ff:ff:ff:ff:ff:ff", "33:33:00:00:00:01", MAC_ADDR, multicast(IPV6_ADDR)]:
- nd_ns = Ether(dst=mac)/IPv6()/ICMPv6ND_NS(tgt=IPV6_ADDR)
- nd_na = iface.sr1(nd_ns, timeout=1)
- assert(nd_na is not None), "expecting answer, got nothing"
- assert(ICMPv6ND_NA in nd_na)
- nd_na = nd_na[ICMPv6ND_NA]
- # check answer content
- assert(nd_na.code == 0)
- assert(nd_na.R == 0)
- assert(nd_na.S == 1)
- assert(nd_na.O == 1)
- assert(nd_na.tgt == IPV6_ADDR)
- # check ND Option
- assert(nd_na.haslayer(ICMPv6NDOptDstLLAddr))
- assert(nd_na.getlayer(ICMPv6NDOptDstLLAddr).lladdr == MAC_ADDR)
- for mac in ["00:00:00:00:00:00", "33:33:33:00:00:01"]:
- nd_ns = Ether(dst="ff:ff:ff:ff:ff:ff")/IPv6()/ICMPv6ND_NS(tgt=IPV6_ADDR)
- nd_na = iface.sr1(nd_ns, timeout=1)
- assert(nd_na is not None), "expecting no answer, got one"
-
-@test
-def test_icmpv6_neighbor_solicitation_other_ip(iface):
- ##### IPv6 Neighbor Solicitation #####
- nd_ns = Ether(dst="ff:ff:ff:ff:ff:ff")/IPv6()/ICMPv6ND_NS(tgt="2020:4141:3030:2020::bdbd")
- nd_na = iface.sr1(nd_ns, timeout=1)
- assert(nd_na is None), "responding to ND_NS for other IP addresses"
-
-@test
-def test_icmpv6_echo_req(iface):
- ##### IPv6 Ping #####
- echo_req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/ICMPv6EchoRequest(data="waytoomanynapkins")
- echo_repl = iface.sr1(echo_req, timeout=1)
- assert(echo_repl is not None), "expecting answer, got nothing"
- assert(ICMPv6EchoReply in echo_repl)
- echo_repl = echo_repl[ICMPv6EchoReply]
- # check answer content
- assert(echo_repl.code == 0)
- assert(echo_repl.data == echo_req.data)
-
-@test
-def test_tcp_syn(iface):
- ##### SYN-ACK #####
- # test a list of ports, randomly generated once
- ports_to_test = [
- 1152, 2003, 2193, 3709, 4054, 6605, 6737, 6875, 7320, 8898, 9513, 9738, 10623, 10723,
- 11253, 12125, 12189, 12873, 14648, 14659, 16242, 16243, 17209, 17492, 17667, 17838,
- 18081, 18682, 18790, 19124, 19288, 19558, 19628, 19789, 20093, 21014, 21459, 21740,
- 24070, 24312, 24576, 26939, 27136, 27165, 27361, 29971, 31088, 33011, 33068, 34990,
- 35093, 35958, 36626, 36789, 37130, 37238, 37256, 37697, 37890, 38958, 42131, 43864,
- 44420, 44655, 44868, 45157, 46213, 46497, 46955, 49049, 49067, 49452, 49480, 50498,
- 50945, 51181, 52890, 53301, 53407, 53417, 53980, 55827, 56483, 58552, 58713, 58836,
- 59362, 59560, 60534, 60555, 60660, 61615, 62402, 62533, 62941, 63240, 63339, 63616,
- 64380, 65438,
- ]
- for p in ports_to_test:
- syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", dport=p)
- syn_ack = iface.sr1(syn, timeout=1)
- assert(syn_ack is not None), "expecting answer, got nothing"
- check_ip_checksum(syn_ack)
- assert(TCP in syn_ack)
- syn_ack = syn_ack[TCP]
- assert(syn_ack.flags == "SA")
-
-@test
-def test_ipv4_tcp_psh_ack(iface):
- ##### PSH-ACK #####
- sport = 26695
- port = 445
- # send PSH-ACK first
- psh_ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", dport=port)/Raw("payload")
- syn_ack = iface.sr1(psh_ack, timeout=1)
- assert(syn_ack is None), "no answer expected, got one"
- # test the anti-injection mechanism
- syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", dport=port)
- syn_ack = iface.sr1(syn, timeout=1)
- assert(syn_ack is not None), "expecting answer, got nothing"
- check_ip_checksum(syn_ack)
- assert(TCP in syn_ack)
- syn_ack = syn_ack[TCP]
- assert(syn_ack.flags == "SA")
- ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", dport=port)
- # should fail because no ack given
- psh_ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", dport=port)
- ack = iface.sr1(psh_ack, timeout=1)
- assert(ack is None), "no answer expected, got one"
- # should get an answer this time
- psh_ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", dport=port, ack=syn_ack.seq + 1)
- ack = iface.sr1(psh_ack, timeout=1)
- assert(ack is not None), "expecting answer, got nothing"
- check_ip_checksum(ack)
- assert(TCP in ack)
- ack = ack[TCP]
- assert(ack.flags == "A")
-
-@test
-def test_ipv6_tcp_psh_ack(iface):
- ##### PSH-ACK #####
- sport = 26695
- port = 445
- # send PSH-ACK first
- psh_ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", dport=port)/Raw("payload")
- syn_ack = iface.sr1(psh_ack, timeout=1)
- assert(syn_ack is None), "no answer expected, got one"
- # test the anti-injection mechanism
- syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", dport=port)
- syn_ack = iface.sr1(syn, timeout=1)
- assert(syn_ack is not None), "expecting answer, got nothing"
- check_ipv6_checksum(syn_ack)
- assert(TCP in syn_ack)
- syn_ack = syn_ack[TCP]
- assert(syn_ack.flags == "SA")
- ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", dport=port)
- # should fail because no ack given
- psh_ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", dport=port)
- ack = iface.sr1(psh_ack, timeout=1)
- assert(ack is None), "no answer expected, got one"
- # should get an answer this time
- psh_ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", dport=port, ack=syn_ack.seq + 1)
- ack = iface.sr1(psh_ack, timeout=1)
- assert(ack is not None), "expecting answer, got nothing"
- check_ipv6_checksum(ack)
- assert(TCP in ack)
- ack = ack[TCP]
- assert(ack.flags == "A")
-
-@test
-def test_ipv4_tcp_http(iface):
- sport = 24592
- dports = [80, 443, 5000, 53228]
- for dport in dports:
- syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", sport=sport, dport=dport)
- syn_ack = iface.sr1(syn, timeout=1)
- assert(syn_ack is not None), "expecting answer, got nothing"
- check_ip_checksum(syn_ack)
- assert(TCP in syn_ack)
- syn_ack = syn_ack[TCP]
- assert(syn_ack.flags == "SA")
- ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1)
- _ = iface.sr1(ack, timeout=1)
- req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n")
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ip_checksum(resp)
- assert(TCP in resp)
- tcp = resp[TCP]
- assert(tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n"))
-
-@test
-def test_ipv6_tcp_http(iface):
- sport = 24592
- dports = [80, 443, 5000, 53228]
- for dport in dports:
- syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", sport=sport, dport=dport)
- syn_ack = iface.sr1(syn, timeout=1)
- assert(syn_ack is not None), "expecting answer, got nothing"
- check_ipv6_checksum(syn_ack)
- assert(TCP in syn_ack)
- syn_ack = syn_ack[TCP]
- assert(syn_ack.flags == "SA")
- ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1)
- _ = iface.sr1(ack, timeout=1)
- req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n")
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ipv6_checksum(resp)
- assert(TCP in resp)
- tcp = resp[TCP]
- assert(tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n"))
-
-@test
-def test_ipv4_udp_http(iface):
- sport = 24592
- dports = [80, 443, 5000, 53228]
- for dport in dports:
- req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n")
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ip_checksum(resp)
- assert(UDP in resp)
- udp = resp[UDP]
- assert(udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n"))
-
-@test
-def test_ipv6_udp_http(iface):
- sport = 24592
- dports = [80, 443, 5000, 53228]
- for dport in dports:
- req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw("GET / HTTP/1.1\r\n\r\n")
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ipv6_checksum(resp)
- assert(UDP in resp)
- udp = resp[UDP]
- assert(udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n"))
-
-@test
-def test_ipv4_tcp_http_ko(iface):
- sport = 24592
- dports = [80, 443, 5000, 53228]
- for dport in dports:
- syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", sport=sport, dport=dport)
- syn_ack = iface.sr1(syn, timeout=1)
- assert(syn_ack is not None), "expecting answer, got nothing"
- check_ip_checksum(syn_ack)
- assert(TCP in syn_ack)
- syn_ack = syn_ack[TCP]
- assert(syn_ack.flags == "SA")
- ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1)
- _ = iface.sr1(ack, timeout=1)
- req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53"))
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ip_checksum(resp)
- assert(TCP in resp)
- assert("P" not in resp[TCP].flags)
- assert(len(resp[TCP].payload) == 0)
-
-@test
-def test_ipv4_udp_http_ko(iface):
- sport = 24592
- dports = [80, 443, 5000, 53228]
- for dport in dports:
- req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53"))
- resp = iface.sr1(req, timeout=1)
- assert(resp is None), "expecting no answer, got one"
-
-@test
-def test_ipv6_tcp_http_ko(iface):
- sport = 24592
- dports = [80, 443, 5000, 53228]
- for dport in dports:
- syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", sport=sport, dport=dport)
- syn_ack = iface.sr1(syn, timeout=1)
- assert(syn_ack is not None), "expecting answer, got nothing"
- check_ipv6_checksum(syn_ack)
- assert(TCP in syn_ack)
- syn_ack = syn_ack[TCP]
- assert(syn_ack.flags == "SA")
- ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1)
- _ = iface.sr1(ack, timeout=1)
- req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53"))
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ipv6_checksum(resp)
- assert(TCP in resp)
- assert("P" not in resp[TCP].flags)
- assert(len(resp[TCP].payload) == 0)
-
-@test
-def test_ipv6_udp_http_ko(iface):
- sport = 24592
- dports = [80, 443, 5000, 53228]
- for dport in dports:
- req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(bytes.fromhex("4f5054494f4e53"))
- resp = iface.sr1(req, timeout=1)
- assert(resp is None), "expecting no answer, got one"
-
-@test
-def test_ipv4_udp_stun(iface):
- sports = [12345, 55555, 80, 43273]
- dports = [80, 800, 8000, 3478]
- payload = bytes.fromhex("000100002112a442000000000000000000000000")
- for sport in sports:
- for dport in dports:
- req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload)
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ip_checksum(resp)
- assert(UDP in resp), "no UDP layer found"
- udp = resp[UDP]
- assert(udp.sport == dport), "unexpected UDP sport: {}".format(udp.sport)
- assert(udp.dport == sport), "unexpected UDP dport: {}".format(udp.dport)
- resp_payload = udp.payload.load
- type_, length, magic = struct.unpack(">HHI", resp_payload[:8])
- tid = resp_payload[8:20]
- data = resp_payload[20:]
- assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_)
- assert(length == 12), "expected length 12, got {}".format(length)
- assert(magic == 0x2112a442), "expected magic 0x2112a442, got 0x{:08x}".format(magic)
- assert(tid == b'\x00' * 12), "expected tid 0x000000000000000000000000, got {:x}".format(tid)
- assert(data == bytes.fromhex("000100080001") + struct.pack(">H", sport) + bytes.fromhex("00000000")), "unexpected data"
-
-@test
-def test_ipv6_udp_stun(iface):
- sports = [12345, 55555, 80, 43273]
- dports = [80, 800, 8000, 3478]
- payload = bytes.fromhex("000100002112a442000000000000000000000000")
- for sport in sports:
- for dport in dports:
- req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload)
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ipv6_checksum(resp)
- assert(UDP in resp)
- udp = resp[UDP]
- assert(udp.sport == dport)
- assert(udp.dport == sport)
- resp_payload = udp.payload.load
- type_, length, magic = struct.unpack(">HHI", resp_payload[:8])
- tid = resp_payload[8:20]
- data = resp_payload[20:]
- assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_)
- assert(length == 24), "expected length 24, got {}".format(length)
- assert(magic == 0x2112a442), "expected magic 0x2112a442, got 0x{:08x}".format(magic)
- assert(tid == b'\x00' * 12), "expected tid 0x000000000000000000000000, got {:x}".format(tid)
- assert(data == bytes.fromhex("000100140002") + struct.pack(">H", sport) + bytes.fromhex("00000000" * 4)), "unexpected data: {}".format(data)
-
-@test
-def test_ipv4_udp_stun_change_port(iface):
- sports = [12345, 55555, 80, 43273]
- dports = [80, 800, 8000, 3478, 65535]
- payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002")
- for sport in sports:
- for dport in dports:
- req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload)
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ip_checksum(resp)
- assert(UDP in resp), "no UDP layer found"
- udp = resp[UDP]
- assert(udp.sport == (dport + 1) % 2**16), "expected answer from UDP/{}, got it from UDP/{}".format((dport + 1) % 2**16, udp.sport)
- assert(udp.dport == sport), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport)
- resp_payload = udp.payload.load
- type_, length = struct.unpack(">HH", resp_payload[:4])
- tid = resp_payload[4:20]
- data = resp_payload[20:]
- assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_)
- assert(length == 12), "expected length 12, got {}".format(length)
- assert(tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c")), "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid
- assert(data == bytes.fromhex("000100080001") + struct.pack(">H", sport) + bytes.fromhex("00000000")), "unexpected data"
-
-@test
-def test_ipv6_udp_stun_change_port(iface):
- sports = [12345, 55555, 80, 43273]
- dports = [80, 800, 8000, 3478, 65535]
- payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002")
- for sport in sports:
- for dport in dports:
- req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(payload)
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ipv6_checksum(resp)
- assert(UDP in resp), "expecting UDP layer in answer, got nothing"
- udp = resp[UDP]
- assert(udp.sport == (dport + 1) % 2**16), "expected answer from UDP/{}, got it from UDP/{}".format((dport + 1) % 2**16, udp.sport)
- assert(udp.dport == sport), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport)
- resp_payload = udp.payload.load
- type_, length = struct.unpack(">HH", resp_payload[:4])
- tid = resp_payload[4:20]
- data = resp_payload[20:]
- assert(type_ == 0x0101), "expected type 0X0101, got 0x{:04x}".format(type_)
- assert(length == 24), "expected length 12, got {}".format(length)
- assert(tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c")), "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid
- assert(data == bytes.fromhex("000100140002") + struct.pack(">H", sport) + bytes.fromhex("00000000" * 4))
-
-@test
-def test_ipv4_tcp_ssh(iface):
- sport = 37183
- dports = [22, 80, 2222, 2022, 23874, 50000]
- for i, dport in enumerate(dports):
- banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5]
- syn = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="S", sport=sport, dport=dport)
- syn_ack = iface.sr1(syn, timeout=1)
- assert(syn_ack is not None), "expecting answer, got nothing"
- check_ip_checksum(syn_ack)
- assert(TCP in syn_ack)
- syn_ack = syn_ack[TCP]
- assert(syn_ack.flags == "SA")
- ack = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1)
- _ = iface.sr1(ack, timeout=1)
- req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(banner + b"\r\n")
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ip_checksum(resp)
- assert(TCP in resp)
- tcp = resp[TCP]
- assert("A" in tcp.flags), "expecting ACK flag, not set (%r)" % tcp.flags
- assert("P" in tcp.flags), "expecting PSH flag, not set (%r)" % tcp.flags
- assert(len(tcp.payload) > 0), "expecting payload, got none"
- assert(tcp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % tcp.payload.load
- assert(tcp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % tcp.payload.load
-
-@test
-def test_ipv4_udp_ssh(iface):
- sport = 37183
- dports = [22, 80, 2222, 2022, 23874, 50000]
- for i, dport in enumerate(dports):
- banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5]
- req = Ether(dst=MAC_ADDR)/IP(dst=IPV4_ADDR)/UDP(sport=sport, dport=dport)/Raw(banner + b"\r\n")
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ip_checksum(resp)
- assert(UDP in resp)
- udp = resp[UDP]
- assert(len(udp.payload) > 0), "expecting payload, got none"
- assert(udp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % udp.payload.load
- assert(udp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % udp.payload.load
-
-@test
-def test_ipv6_tcp_ssh(iface):
- sport = 37183
- dports = [22, 80, 2222, 2022, 23874, 50000]
- for i, dport in enumerate(dports):
- banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5]
- syn = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="S", sport=sport, dport=dport)
- syn_ack = iface.sr1(syn, timeout=1)
- assert(syn_ack is not None), "expecting answer, got nothing"
- check_ipv6_checksum(syn_ack)
- assert(TCP in syn_ack)
- syn_ack = syn_ack[TCP]
- assert(syn_ack.flags == "SA")
- ack = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="A", sport=sport, dport=dport, ack=syn_ack.seq + 1)
- _ = iface.sr1(ack, timeout=1)
- req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/TCP(flags="PA", ack=syn_ack.seq + 1, sport=sport, dport=dport)/Raw(banner + b"\r\n")
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ipv6_checksum(resp)
- assert(TCP in resp)
- tcp = resp[TCP]
- assert("A" in tcp.flags), "expecting ACK flag, not set (%r)" % tcp.flags
- assert("P" in tcp.flags), "expecting PSH flag, not set (%r)" % tcp.flags
- assert(len(tcp.payload) > 0), "expecting payload, got none"
- assert(tcp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % tcp.payload.load
- assert(tcp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % tcp.payload.load
-
-@test
-def test_ipv6_udp_ssh(iface):
- sport = 37183
- dports = [22, 80, 2222, 2022, 23874, 50000]
- for i, dport in enumerate(dports):
- banner = [b"SSH-2.0-AsyncSSH_2.1.0", b"SSH-2.0-PuTTY", b"SSH-2.0-libssh2_1.4.3", b"SSH-2.0-Go", b"SSH-2.0-PUTTY"][i % 5]
- req = Ether(dst=MAC_ADDR)/IPv6(dst=IPV6_ADDR)/UDP(sport=sport, dport=dport)/Raw(banner + b"\r\n")
- resp = iface.sr1(req, timeout=1)
- assert(resp is not None), "expecting answer, got nothing"
- check_ipv6_checksum(resp)
- assert(UDP in resp)
- udp = resp[UDP]
- assert(len(udp.payload) > 0), "expecting payload, got none"
- assert(udp.payload.load.startswith(b"SSH-2.0-")), "unexpected banner: %r" % udp.payload.load
- assert(udp.payload.load.endswith(b"\r\n")), "unexpected banner: %r" % udp.payload.load
-
-def test_all(iface):
- global tests
- # execute tests
- for t in tests:
- t(iface)
+for test in ENABLED_TESTS:
+ importlib.import_module(".tests." + test, package="src")
diff --git a/test/src/conf.py b/test/src/conf.py
index f7fbd2b..18affd9 100644
--- a/test/src/conf.py
+++ b/test/src/conf.py
@@ -16,5 +16,5 @@
IPV4_ADDR = "192.0.0.1"
IPV6_ADDR = "2001:41d0::ab32:bdb8"
-MAC_ADDR = "52:1c:4e:c2:a4:1f"
+MAC_ADDR = "52:1c:4e:c2:a4:1f"
OUTDIR = "test/res/"
diff --git a/test/src/core.py b/test/src/core.py
new file mode 100644
index 0000000..829f314
--- /dev/null
+++ b/test/src/core.py
@@ -0,0 +1,102 @@
+# This file is part of masscanned.
+# Copyright 2021 - 2025 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+import logging
+
+from scapy.compat import raw
+from scapy.layers.inet import IP
+from scapy.layers.inet6 import IPv6
+
+
+def setup_logs():
+ log = logging.getLogger()
+ log.setLevel(logging.DEBUG)
+ if not log.handlers:
+ ch = logging.StreamHandler()
+ ch.setFormatter(logging.Formatter("%(levelname)s\t%(message)s"))
+ ch.setLevel(logging.DEBUG)
+ log.addHandler(ch)
+ return log
+
+
+LOG = setup_logs()
+TESTS = []
+ERRORS = []
+
+
+# decorator to automatically add a function to tests
+def test(f):
+ OK = "\033[1mOK\033[0m"
+ KO = "\033[1m\033[1;%dmKO\033[0m" % 31
+ fname = f.__name__.ljust(50, ".")
+
+ def w(m):
+ try:
+ # check that masscanned is still running
+ assert m.poll() is None, "masscanned not running"
+ f()
+ # check that masscanned is still running
+ assert m.poll() is None, "masscanned terminated unexpectedly"
+ LOG.info("{}{}".format(fname, OK))
+ except AssertionError as e:
+ LOG.error("{}{}: {}".format(fname, KO, e))
+ ERRORS.append(fname)
+
+ TESTS.append(w)
+ return w
+
+
+def test_all(m):
+ # execute tests
+ for t in TESTS:
+ # perform unit test
+ t(m)
+ LOG.info(f"\033[1mRan {len(TESTS)} tests with {len(ERRORS)} errors\033[0m")
+ return len(ERRORS)
+
+
+def multicast(ip6):
+ a, b = ip6.split(":")[-2:]
+ mac = ["33", "33", "ff"]
+ if len(a) == 4:
+ mac.append(a[2:])
+ else:
+ mac.append("00")
+ if len(b) >= 2:
+ mac.append(b[:2])
+ else:
+ mac.append("00")
+ if len(b) >= 4:
+ mac.append(b[2:])
+ else:
+ mac.append("00")
+ return ":".join(mac)
+
+
+def check_ip_checksum(pkt):
+ assert IP in pkt, "no IP layer found"
+ ip_pkt = pkt[IP]
+ chksum = ip_pkt.chksum
+ del ip_pkt.chksum
+ assert IP(raw(ip_pkt)).chksum == chksum, "bad IPv4 checksum"
+
+
+def check_ipv6_checksum(pkt):
+ assert IPv6 in pkt, "no IP layer found"
+ ip_pkt = pkt[IPv6]
+ chksum = ip_pkt.chksum
+ del ip_pkt.chksum
+ assert IPv6(raw(ip_pkt)).chksum == chksum, "bad IPv6 checksum"
diff --git a/test/src/tests/__init__.py b/test/src/tests/__init__.py
new file mode 100644
index 0000000..831f613
--- /dev/null
+++ b/test/src/tests/__init__.py
@@ -0,0 +1,15 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
diff --git a/test/src/tests/arp.py b/test/src/tests/arp.py
new file mode 100644
index 0000000..4f65cb7
--- /dev/null
+++ b/test/src/tests/arp.py
@@ -0,0 +1,51 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from scapy.layers.l2 import Ether, ARP, ETHER_BROADCAST
+from scapy.sendrecv import srp1
+
+from ..conf import IPV4_ADDR, MAC_ADDR
+from ..core import test
+
+
+@test
+def test_arp_req():
+ ##### ARP #####
+ arp_req = Ether(dst=ETHER_BROADCAST) / ARP(pdst=IPV4_ADDR)
+ arp_repl = srp1(arp_req, timeout=1)
+ assert arp_repl is not None, "expecting answer, got nothing"
+ assert ARP in arp_repl, "no ARP layer found"
+ arp_repl = arp_repl[ARP]
+ # check answer
+ ## op is "is-at"
+ assert arp_repl.op == 2, "unexpected ARP op: {}".format(arp_repl.op)
+ ## answer for the requested IP
+ assert arp_repl.psrc == arp_req.pdst, "unexpected ARP psrc: {}".format(
+ arp_repl.psrc
+ )
+ assert arp_repl.pdst == arp_req.psrc, "unexpected ARP pdst: {}".format(
+ arp_repl.pdst
+ )
+ ## answer is expected MAC address
+ assert arp_repl.hwsrc == MAC_ADDR, "unexpected ARP hwsrc: {}".format(arp_repl.hwsrc)
+
+
+@test
+def test_arp_req_other_ip():
+ ##### ARP #####
+ arp_req = Ether(dst=ETHER_BROADCAST) / ARP(pdst="1.2.3.4")
+ arp_repl = srp1(arp_req, timeout=1)
+ assert arp_repl is None, "responding to ARP requests for other IP addresses"
diff --git a/test/src/tests/dns.py b/test/src/tests/dns.py
new file mode 100644
index 0000000..93ab1ac
--- /dev/null
+++ b/test/src/tests/dns.py
@@ -0,0 +1,158 @@
+# This file is part of masscanned.
+# Copyright 2022 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from scapy.compat import raw
+from scapy.layers.dns import DNS, DNSQR
+from scapy.layers.inet import IP, UDP
+from scapy.layers.l2 import Ether
+from scapy.sendrecv import srp1
+
+from ..conf import IPV4_ADDR, MAC_ADDR
+from ..core import test, check_ip_checksum
+
+
+@test
+def test_ipv4_udp_dns_in_a():
+ sports = [53, 13274, 0]
+ dports = [53, 5353, 80, 161, 24732]
+ for sport in sports:
+ for dport in dports:
+ for domain in ["example.com", "www.example.com", "masscan.ned"]:
+ qd = DNSQR(qname=domain, qtype="A", qclass="IN")
+ dns_req = DNS(id=1234, rd=False, opcode=0, qd=qd)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / dns_req
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert UDP in resp, "no UDP layer found"
+ udp = resp[UDP]
+ assert (
+ udp.sport == dport
+ ), f"unexpected UDP sport: {udp.sport!r} ({domain})"
+ assert (
+ udp.dport == sport
+ ), f"unexpected UDP dport: {udp.dport!r} ({domain})"
+ if DNS not in udp:
+ try:
+ dns_rep = DNS(udp.load)
+ except Exception:
+ raise AssertionError("no DNS layer found")
+ else:
+ dns_rep = udp[DNS]
+ assert (
+ dns_rep.id == 1234
+ ), f"unexpected id value: {dns_rep.id!r} ({domain})"
+ assert dns_rep.qr, "unexpected qr value"
+ assert dns_rep.opcode == 0, "unexpected opcode value"
+ assert dns_rep.aa, "unexpected aa value"
+ assert not dns_rep.tc, "unexpected tc value"
+ assert not dns_rep.rd, "unexpected rd value"
+ assert not dns_rep.ra, "unexpected ra value"
+ assert dns_rep.z == 0, "unexpected z value"
+ assert dns_rep.rcode == 0, "unexpected rcode value"
+ assert (
+ dns_rep.qdcount == 1
+ ), f"unexpected qdcount value: {dns_rep.qdcount!r} vs 1 ({domain})"
+ assert dns_rep.ancount == 1, "unexpected ancount value"
+ assert dns_rep.nscount == 0, "unexpected nscount value"
+ assert dns_rep.arcount == 0, "unexpected arcount value"
+ assert raw(dns_rep.qd[0]) == raw(
+ dns_req.qd[0]
+ ), "query in request and response do not match"
+ assert raw(dns_rep.qd[0].qname) == raw(
+ dns_req.qd[0].qname
+ ), "qname query in request and response do not match"
+ assert (
+ dns_rep.an[0].rrname == dns_req.qd[0].qname
+ ), "rrname in answer does not match qname in request"
+ assert (
+ dns_rep.an[0].rclass == dns_req.qd[0].qclass
+ ), "class in answer does not match query"
+ assert (
+ dns_rep.an[0].type == dns_req.qd[0].qtype
+ ), "type in answer does not match query"
+ assert dns_rep.an[0].rdata == IPV4_ADDR
+
+
+@test
+def test_ipv4_udp_dns_in_a_multiple_queries():
+ sports = [53, 13274, 12198, 888, 0]
+ dports = [53, 5353, 80, 161, 24732]
+ for sport in sports:
+ for dport in dports:
+ qd = [
+ DNSQR(qname="www.example1.com", qtype="A", qclass="IN"),
+ DNSQR(qname="www.example2.com", qtype="A", qclass="IN"),
+ DNSQR(qname="www.example3.com", qtype="A", qclass="IN"),
+ ]
+ dns_req = DNS(id=1234, rd=False, opcode=0, qd=qd)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / dns_req
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert UDP in resp, "no UDP layer found"
+ udp = resp[UDP]
+ assert udp.sport == dport, "unexpected UDP sport: {}".format(udp.sport)
+ assert udp.dport == sport, "unexpected UDP dport: {}".format(udp.dport)
+ if DNS not in udp:
+ try:
+ dns_rep = DNS(udp.load)
+ except Exception:
+ raise AssertionError("no DNS layer found")
+ else:
+ dns_rep = udp[DNS]
+ assert dns_rep.id == 1234, f"unexpected id value: {dns_rep.id}"
+ assert dns_rep.qr, "unexpected qr value"
+ assert dns_rep.opcode == 0, "unexpected opcode value"
+ assert dns_rep.aa, "unexpected aa value"
+ assert not dns_rep.tc, "unexpected tc value"
+ assert not dns_rep.rd, "unexpected rd value"
+ assert not dns_rep.ra, "unexpected ra value"
+ assert dns_rep.z == 0, "unexpected z value"
+ assert dns_rep.rcode == 0, "unexpected rcode value"
+ assert (
+ dns_rep.qdcount == 3
+ ), f"unexpected qdcount value: {dns_rep.qdcount} vs 3"
+ assert dns_rep.ancount == 3, "unexpected ancount value"
+ assert dns_rep.nscount == 0, "unexpected nscount value"
+ assert dns_rep.arcount == 0, "unexpected arcount value"
+ for i, q in enumerate(qd):
+ assert raw(dns_rep.qd[i]) == raw(
+ dns_req.qd[i]
+ ), f"query in request and response do not match ({i})"
+ assert raw(dns_rep.qd[i].qname) == raw(
+ dns_req.qd[i].qname
+ ), f"qname query in request and response do not match ({i})"
+ assert (
+ dns_rep.an[i].rrname == dns_req.qd[i].qname
+ ), f"rrname in answer does not match qname in request ({i})"
+ assert (
+ dns_rep.an[i].rclass == dns_req.qd[i].qclass
+ ), f"class in answer does not match query ({i})"
+ assert (
+ dns_rep.an[i].type == dns_req.qd[i].qtype
+ ), f"type in answer does not match query ({i})"
+ assert dns_rep.an[i].rdata == IPV4_ADDR
diff --git a/test/src/tests/ghost.py b/test/src/tests/ghost.py
new file mode 100644
index 0000000..303e58e
--- /dev/null
+++ b/test/src/tests/ghost.py
@@ -0,0 +1,87 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+import struct
+import zlib
+
+from scapy.compat import raw
+from scapy.layers.inet import IP, TCP
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+from scapy.sendrecv import srp1
+from scapy.volatile import RandInt
+
+from ..conf import IPV4_ADDR, MAC_ADDR
+from ..core import test, check_ip_checksum
+
+
+@test
+def test_ipv4_tcp_ghost():
+ sport = 37184
+ dports = [22, 23874]
+ for dport in dports:
+ seq_init = int(RandInt())
+ banner = b"Gh0st\xad\x00\x00\x00\xe0\x00\x00\x00x\x9cKS``\x98\xc3\xc0\xc0\xc0\x06\xc4\x8c@\xbcQ\x96\x81\x81\tH\x07\xa7\x16\x95e&\xa7*\x04$&g+\x182\x94\xf6\xb000\xac\xa8rc\x00\x01\x11\xa0\x82\x1f\\`&\x83\xc7K7\x86\x19\xe5n\x0c9\x95n\x0c;\x84\x0f3\xac\xe8sch\xa8^\xcf4'J\x97\xa9\x82\xe30\xc3\x91h]&\x90\xf8\xce\x97S\xcbA4L?2=\xe1\xc4\x92\x86\x0b@\xf5`\x0cT\x1f\xae\xaf]\nr\x0b\x03#\xa3\xdc\x02~\x06\x86\x03+\x18m\xc2=\xfdtC,C\xfdL<<==\\\x9d\x19\x88\x00\xe5 \x02\x00T\xf5+\\"
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="S", sport=sport, dport=dport, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ip_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA"
+ ack = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="A",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ )
+ _ = srp1(ack, timeout=1)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ / Raw(banner)
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ tcp = resp[TCP]
+ assert "A" in tcp.flags, "expecting ACK flag, not set (%r)" % tcp.flags
+ assert "P" in tcp.flags, "expecting PSH flag, not set (%r)" % tcp.flags
+ data = raw(tcp.payload)
+ assert data, "expecting payload, got none"
+ assert data.startswith(b"Gh0st"), "unexpected banner: %r" % tcp.payload.load
+ data_len, uncompressed_len = struct.unpack(".
+
+from scapy.layers.inet import IP, TCP, UDP
+from scapy.layers.inet6 import IPv6
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+from scapy.sendrecv import srp1
+from scapy.volatile import RandInt
+
+from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR
+from ..core import test, check_ip_checksum, check_ipv6_checksum
+
+
+@test
+def test_ipv4_tcp_http():
+ sport = 24592
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="S", sport=sport, dport=dport, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ip_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags
+ ack = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="A",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ )
+ _ = srp1(ack, timeout=1)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ / Raw("GET / HTTP/1.1\r\n\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ tcp = resp[TCP]
+ assert tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")
+
+
+@test
+def test_ipv4_tcp_http_segmented():
+ sport = 24593
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="S", sport=sport, dport=dport, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ip_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags
+ ack = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="A",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ )
+ _ = srp1(ack, timeout=1)
+ # request is not complete yet
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ / Raw("GET / HTTP/1.1\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ assert resp[TCP].flags == "A", (
+ 'expecting TCP flag "A", got %r' % resp[TCP].flags
+ )
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + len(req) + 1,
+ ack=syn_ack.seq + 1,
+ )
+ / Raw("\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ tcp = resp[TCP]
+ assert tcp.flags == "PA"
+ assert tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")
+
+
+@test
+def test_ipv4_tcp_http_incomplete():
+ sport = 24595
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="S", sport=sport, dport=dport, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ip_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags
+ ack = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="A",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ )
+ _ = srp1(ack, timeout=1)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ # purposedly incomplete request (missing additionnal ending \r\n)
+ / Raw("GET / HTTP/1.1\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting an answer, got none"
+ check_ip_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ tcp = resp[TCP]
+ assert tcp.flags == "A", "expecting TCP flag A, got {}".format(tcp.flags)
+
+
+@test
+def test_ipv6_tcp_http():
+ sport = 24594
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(flags="S", sport=sport, dport=dport, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA"
+ ack = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(
+ flags="A",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ )
+ _ = srp1(ack, timeout=1)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ / Raw("GET / HTTP/1.1\r\n\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ tcp = resp[TCP]
+ assert tcp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")
+
+
+@test
+def test_ipv4_udp_http():
+ sport = 24592
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw("GET / HTTP/1.1\r\n\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert UDP in resp
+ udp = resp[UDP]
+ assert udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")
+
+
+@test
+def test_ipv6_udp_http():
+ sport = 24592
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw("GET / HTTP/1.1\r\n\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(resp)
+ assert UDP in resp
+ udp = resp[UDP]
+ assert udp.payload.load.startswith(b"HTTP/1.1 401 Unauthorized\n")
+
+
+@test
+def test_ipv4_tcp_http_ko():
+ sport = 24596
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="S", sport=sport, dport=dport, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ip_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA"
+ ack = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="A",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ )
+ _ = srp1(ack, timeout=1)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ / Raw(bytes.fromhex("4f5054494f4e53"))
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ assert "P" not in resp[TCP].flags
+ assert len(resp[TCP].payload) == 0
+
+
+@test
+def test_ipv4_udp_http_ko():
+ sport = 24592
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw(bytes.fromhex("4f5054494f4e53"))
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is None, "expecting no answer, got one"
+
+
+@test
+def test_ipv6_tcp_http_ko():
+ sport = 24597
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(flags="S", sport=sport, dport=dport, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA"
+ ack = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(
+ flags="A",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ )
+ _ = srp1(ack, timeout=1)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ / Raw(bytes.fromhex("4f5054494f4e53"))
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ assert "P" not in resp[TCP].flags
+ assert len(resp[TCP].payload) == 0
+
+
+@test
+def test_ipv6_udp_http_ko():
+ sport = 24592
+ dports = [80, 443, 5000, 53228]
+ for dport in dports:
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw(bytes.fromhex("4f5054494f4e53"))
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is None, "expecting no answer, got one"
diff --git a/test/src/tests/icmpv4.py b/test/src/tests/icmpv4.py
new file mode 100644
index 0000000..7722148
--- /dev/null
+++ b/test/src/tests/icmpv4.py
@@ -0,0 +1,45 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from scapy.layers.inet import IP, ICMP
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+from scapy.sendrecv import srp1
+
+from ..conf import IPV4_ADDR, MAC_ADDR
+from ..core import test, check_ip_checksum
+
+
+@test
+def test_icmpv4_echo_req():
+ ##### ICMPv4 #####
+ icmp_req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / ICMP(type=8, code=0)
+ / Raw("idrinkwaytoomuchcoffee")
+ )
+ icmp_repl = srp1(icmp_req, timeout=1)
+ assert icmp_repl is not None, "expecting answer, got nothing"
+ check_ip_checksum(icmp_repl)
+ assert ICMP in icmp_repl
+ icmp_repl = icmp_repl[ICMP]
+ # check answer
+ ## type is "echo-reply"
+ assert icmp_repl.type == 0
+ assert icmp_repl.code == 0
+ ## data is the same as sent
+ assert icmp_repl.load == icmp_req.load
diff --git a/test/src/tests/icmpv6.py b/test/src/tests/icmpv6.py
new file mode 100644
index 0000000..13a8817
--- /dev/null
+++ b/test/src/tests/icmpv6.py
@@ -0,0 +1,87 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from scapy.layers.inet6 import (
+ ICMPv6EchoReply,
+ ICMPv6EchoRequest,
+ ICMPv6NDOptDstLLAddr,
+ ICMPv6ND_NA,
+ ICMPv6ND_NS,
+ IPv6,
+)
+from scapy.layers.l2 import Ether
+from scapy.sendrecv import srp1
+
+from ..conf import IPV6_ADDR, MAC_ADDR
+from ..core import test, multicast
+
+
+@test
+def test_icmpv6_neighbor_solicitation():
+ ##### IPv6 Neighbor Solicitation #####
+ for mac in [
+ "ff:ff:ff:ff:ff:ff",
+ "33:33:00:00:00:01",
+ MAC_ADDR,
+ multicast(IPV6_ADDR),
+ ]:
+ nd_ns = Ether(dst=mac) / IPv6() / ICMPv6ND_NS(tgt=IPV6_ADDR)
+ nd_na = srp1(nd_ns, timeout=1)
+ assert nd_na is not None, "expecting answer, got nothing"
+ assert ICMPv6ND_NA in nd_na
+ nd_na = nd_na[ICMPv6ND_NA]
+ # check answer content
+ assert nd_na.code == 0
+ assert nd_na.R == 0
+ assert nd_na.S == 1
+ assert nd_na.O == 1 # noqa: E741
+ assert nd_na.tgt == IPV6_ADDR
+ # check ND Option
+ assert nd_na.haslayer(ICMPv6NDOptDstLLAddr)
+ assert nd_na.getlayer(ICMPv6NDOptDstLLAddr).lladdr == MAC_ADDR
+ for mac in ["00:00:00:00:00:00", "33:33:33:00:00:01"]:
+ nd_ns = Ether(dst="ff:ff:ff:ff:ff:ff") / IPv6() / ICMPv6ND_NS(tgt=IPV6_ADDR)
+ nd_na = srp1(nd_ns, timeout=1)
+ assert nd_na is not None, "expecting no answer, got one"
+
+
+@test
+def test_icmpv6_neighbor_solicitation_other_ip():
+ ##### IPv6 Neighbor Solicitation #####
+ nd_ns = (
+ Ether(dst="ff:ff:ff:ff:ff:ff")
+ / IPv6()
+ / ICMPv6ND_NS(tgt="2020:4141:3030:2020::bdbd")
+ )
+ nd_na = srp1(nd_ns, timeout=1)
+ assert nd_na is None, "responding to ND_NS for other IP addresses"
+
+
+@test
+def test_icmpv6_echo_req():
+ ##### IPv6 Ping #####
+ echo_req = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / ICMPv6EchoRequest(data="waytoomanynapkins")
+ )
+ echo_repl = srp1(echo_req, timeout=1)
+ assert echo_repl is not None, "expecting answer, got nothing"
+ assert ICMPv6EchoReply in echo_repl
+ echo_repl = echo_repl[ICMPv6EchoReply]
+ # check answer content
+ assert echo_repl.code == 0
+ assert echo_repl.data == echo_req.data
diff --git a/test/src/tests/ip.py b/test/src/tests/ip.py
new file mode 100644
index 0000000..f87fb75
--- /dev/null
+++ b/test/src/tests/ip.py
@@ -0,0 +1,50 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from scapy.layers.inet import IP, ICMP
+from scapy.layers.l2 import Ether
+from scapy.sendrecv import srp1
+
+from ..conf import IPV4_ADDR, MAC_ADDR
+from ..core import test, check_ip_checksum
+
+
+@test
+def test_ipv4_req():
+ ##### IP #####
+ ip_req = Ether(dst=MAC_ADDR) / IP(dst=IPV4_ADDR, id=0x1337) / ICMP(type=8, code=0)
+ ip_repl = srp1(ip_req, timeout=1)
+ assert ip_repl is not None, "expecting answer, got nothing"
+ check_ip_checksum(ip_repl)
+ assert IP in ip_repl, "no IP layer in response"
+ ip_repl = ip_repl[IP]
+ assert ip_repl.id == 0, "IP identification unexpected"
+
+
+@test
+def test_eth_req_other_mac():
+ #### ETH ####
+ ip_req = Ether(dst="00:00:00:11:11:11") / IP(dst=IPV4_ADDR) / ICMP(type=8, code=0)
+ ip_repl = srp1(ip_req, timeout=1)
+ assert ip_repl is None, "responding to other MAC addresses"
+
+
+@test
+def test_ipv4_req_other_ip():
+ ##### IP #####
+ ip_req = Ether(dst=MAC_ADDR) / IP(dst="1.2.3.4") / ICMP(type=8, code=0)
+ ip_repl = srp1(ip_req, timeout=1)
+ assert ip_repl is None, "responding to other IP addresses"
diff --git a/test/src/tests/rpc.py b/test/src/tests/rpc.py
new file mode 100644
index 0000000..82c97a6
--- /dev/null
+++ b/test/src/tests/rpc.py
@@ -0,0 +1,112 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from subprocess import check_call
+from tempfile import NamedTemporaryFile
+import json
+import os
+import re
+
+from ivre.db import DBNmap
+
+from ..conf import IPV4_ADDR
+from ..core import test
+
+
+@test
+def test_rpc_nmap():
+ for scan in "SU":
+ with NamedTemporaryFile(delete=False) as xml_result:
+ check_call(
+ [
+ "nmap",
+ "-n",
+ "-vv",
+ "-oX",
+ "-",
+ IPV4_ADDR,
+ f"-s{scan}V",
+ "-p",
+ "111",
+ "--script",
+ "rpcinfo,rpc-grind",
+ ],
+ stdout=xml_result,
+ )
+ with NamedTemporaryFile(delete=False, mode="w") as json_result:
+ DBNmap(output=json_result).store_scan(xml_result.name)
+ os.unlink(xml_result.name)
+ with open(json_result.name) as fdesc:
+ results = [json.loads(line) for line in fdesc]
+ os.unlink(json_result.name)
+ assert len(results) == 1, f"Expected 1 result, got {len(results)}"
+ result = results[0]
+ assert len(result["ports"]) == 1, f"Expected 1 port, got {len(result['ports'])}"
+ port = result["ports"][0]
+ assert port["port"] == 111, f"Expected port 111, got {port['port']}"
+ assert port["protocol"] == (
+ "tcp" if scan == "S" else "udp"
+ ), f"Unexpected proto {port['protocol']} for scan {scan}"
+ assert port["service_name"] in {
+ "nfs",
+ "rpcbind",
+ "rstatd",
+ "rusersd",
+ }, f"Unexpected service_name: {port['service_name']}"
+ assert port["service_extrainfo"] in {
+ "RPC #100000",
+ "RPC #100001",
+ "RPC #100002",
+ "RPC #100003",
+ }, f"Unexpected service_extrainfo: {port['service_extrainfo']}"
+ assert (
+ len(port["scripts"]) == 1
+ ), f"Expected 1 script, got {len(port['scripts'])}"
+ script = port["scripts"][0]
+ assert script["id"] == "rpcinfo", "Expected rpcinfo script, not found"
+ assert (
+ len(script["rpcinfo"]) == 1
+ ), f"Expected 1 rpcinfo, got {len(script['rpcinfo'])}"
+
+
+@test
+def test_rpcinfo():
+ with NamedTemporaryFile(delete=False) as rpcout:
+ check_call(["rpcinfo", "-p", IPV4_ADDR], stdout=rpcout)
+ with open(rpcout.name) as fdesc:
+ found = []
+ for line in fdesc:
+ line = line.split()
+ if line[0] == "program":
+ # header
+ continue
+ assert line[0] == "100000", f"Expected program 100000, got {line[0]}"
+ found.append(int(line[1]))
+ assert len(found) == 3, f"Expected three versions, got {found}"
+ for i in range(2, 5):
+ assert i in found, f"Missing version {i} in {found}"
+ os.unlink(rpcout.name)
+ with NamedTemporaryFile(delete=False) as rpcout:
+ check_call(["rpcinfo", "-u", IPV4_ADDR, "100000"], stdout=rpcout)
+ with open(rpcout.name) as fdesc:
+ found = []
+ expr = re.compile("^program 100000 version ([0-9]) ready and waiting$")
+ for line in fdesc:
+ found.append(int(expr.search(line.strip()).group(1)))
+ assert len(found) == 3, f"Expected three versions, got {found}"
+ for i in range(2, 5):
+ assert i in found, f"Missing version {i} in {found}"
+ os.unlink(rpcout.name)
diff --git a/test/src/tests/smb.py b/test/src/tests/smb.py
new file mode 100644
index 0000000..3ce421d
--- /dev/null
+++ b/test/src/tests/smb.py
@@ -0,0 +1,65 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+import subprocess
+
+from ..core import test
+from ..conf import IPV4_ADDR
+
+
+@test
+def test_smb1_network_req():
+ proc = subprocess.Popen(
+ [
+ "smbclient",
+ "-U ''",
+ "-N",
+ "-d 6",
+ "-t 1",
+ "-L",
+ IPV4_ADDR,
+ "--option=client min protocol=NT1",
+ ],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ )
+ out, _ = proc.communicate()
+ assert f"Connecting to {IPV4_ADDR} at port 445" in out, "\n" + out
+ assert "session request ok" in out, "\n" + out
+ assert f"negotiated dialect[NT1] against server[{IPV4_ADDR}]" in out, "\n" + out
+
+
+@test
+def test_smb2_network_req():
+ proc = subprocess.Popen(
+ [
+ "smbclient",
+ "-U ''",
+ "-N",
+ "-d 5",
+ "-t 1",
+ "-L",
+ IPV4_ADDR,
+ ],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ )
+ out, _ = proc.communicate()
+ assert f"Connecting to {IPV4_ADDR} at port 445" in out, "\n" + out
+ assert "session request ok" in out, "\n" + out
+ assert f"negotiated dialect[SMB2_02] against server[{IPV4_ADDR}]" in out, "\n" + out
diff --git a/test/src/tests/ssh.py b/test/src/tests/ssh.py
new file mode 100644
index 0000000..31501e4
--- /dev/null
+++ b/test/src/tests/ssh.py
@@ -0,0 +1,217 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from scapy.layers.inet import IP, TCP, UDP
+from scapy.layers.inet6 import IPv6
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+from scapy.sendrecv import srp1
+from scapy.volatile import RandInt
+
+from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR
+from ..core import test, check_ip_checksum, check_ipv6_checksum
+
+
+@test
+def test_ipv4_tcp_ssh():
+ sport = 37183
+ dports = [22, 80, 2222, 2022, 23874, 50000]
+ for i, dport in enumerate(dports):
+ seq_init = int(RandInt())
+ banner = [
+ b"SSH-2.0-AsyncSSH_2.1.0",
+ b"SSH-2.0-PuTTY",
+ b"SSH-2.0-libssh2_1.4.3",
+ b"SSH-2.0-Go",
+ b"SSH-2.0-PUTTY",
+ ][i % 5]
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="S", sport=sport, dport=dport, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ip_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA"
+ ack = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="A",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ )
+ _ = srp1(ack, timeout=1)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ / Raw(banner + b"\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ tcp = resp[TCP]
+ assert "A" in tcp.flags, "expecting ACK flag, not set (%r)" % tcp.flags
+ assert "P" in tcp.flags, "expecting PSH flag, not set (%r)" % tcp.flags
+ assert len(tcp.payload) > 0, "expecting payload, got none"
+ assert tcp.payload.load.startswith(b"SSH-2.0-"), (
+ "unexpected banner: %r" % tcp.payload.load
+ )
+ assert tcp.payload.load.endswith(b"\r\n"), (
+ "unexpected banner: %r" % tcp.payload.load
+ )
+
+
+@test
+def test_ipv4_udp_ssh():
+ sport = 37183
+ dports = [22, 80, 2222, 2022, 23874, 50000]
+ for i, dport in enumerate(dports):
+ banner = [
+ b"SSH-2.0-AsyncSSH_2.1.0",
+ b"SSH-2.0-PuTTY",
+ b"SSH-2.0-libssh2_1.4.3",
+ b"SSH-2.0-Go",
+ b"SSH-2.0-PUTTY",
+ ][i % 5]
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw(banner + b"\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert UDP in resp
+ udp = resp[UDP]
+ assert len(udp.payload) > 0, "expecting payload, got none"
+ assert udp.payload.load.startswith(b"SSH-2.0-"), (
+ "unexpected banner: %r" % udp.payload.load
+ )
+ assert udp.payload.load.endswith(b"\r\n"), (
+ "unexpected banner: %r" % udp.payload.load
+ )
+
+
+@test
+def test_ipv6_tcp_ssh():
+ sport = 37183
+ dports = [22, 80, 2222, 2022, 23874, 50000]
+ for i, dport in enumerate(dports):
+ seq_init = int(RandInt())
+ banner = [
+ b"SSH-2.0-AsyncSSH_2.1.0",
+ b"SSH-2.0-PuTTY",
+ b"SSH-2.0-libssh2_1.4.3",
+ b"SSH-2.0-Go",
+ b"SSH-2.0-PUTTY",
+ ][i % 5]
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(flags="S", sport=sport, dport=dport, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA"
+ ack = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(
+ flags="A",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ )
+ _ = srp1(ack, timeout=1)
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(
+ flags="PA",
+ sport=sport,
+ dport=dport,
+ seq=seq_init + 1,
+ ack=syn_ack.seq + 1,
+ )
+ / Raw(banner + b"\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(resp)
+ assert TCP in resp, "expecting TCP, got %r" % resp.summary()
+ tcp = resp[TCP]
+ assert "A" in tcp.flags, "expecting ACK flag, not set (%r)" % tcp.flags
+ assert "P" in tcp.flags, "expecting PSH flag, not set (%r)" % tcp.flags
+ assert len(tcp.payload) > 0, "expecting payload, got none"
+ assert tcp.payload.load.startswith(b"SSH-2.0-"), (
+ "unexpected banner: %r" % tcp.payload.load
+ )
+ assert tcp.payload.load.endswith(b"\r\n"), (
+ "unexpected banner: %r" % tcp.payload.load
+ )
+
+
+@test
+def test_ipv6_udp_ssh():
+ sport = 37183
+ dports = [22, 80, 2222, 2022, 23874, 50000]
+ for i, dport in enumerate(dports):
+ banner = [
+ b"SSH-2.0-AsyncSSH_2.1.0",
+ b"SSH-2.0-PuTTY",
+ b"SSH-2.0-libssh2_1.4.3",
+ b"SSH-2.0-Go",
+ b"SSH-2.0-PUTTY",
+ ][i % 5]
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw(banner + b"\r\n")
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(resp)
+ assert UDP in resp
+ udp = resp[UDP]
+ assert len(udp.payload) > 0, "expecting payload, got none"
+ assert udp.payload.load.startswith(b"SSH-2.0-"), (
+ "unexpected banner: %r" % udp.payload.load
+ )
+ assert udp.payload.load.endswith(b"\r\n"), (
+ "unexpected banner: %r" % udp.payload.load
+ )
diff --git a/test/src/tests/stun.py b/test/src/tests/stun.py
new file mode 100644
index 0000000..4062ec9
--- /dev/null
+++ b/test/src/tests/stun.py
@@ -0,0 +1,196 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from socket import AF_INET6
+import struct
+
+from scapy.layers.inet import IP, UDP
+from scapy.layers.inet6 import IPv6
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+from scapy.pton_ntop import inet_pton
+from scapy.sendrecv import srp1
+
+from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR
+from ..core import test, check_ip_checksum, check_ipv6_checksum
+
+
+@test
+def test_ipv4_udp_stun():
+ sports = [12345, 55555, 80, 43273]
+ dports = [80, 800, 8000, 3478]
+ payload = bytes.fromhex("000100002112a442000000000000000000000000")
+ for sport in sports:
+ for dport in dports:
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw(payload)
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert UDP in resp, "no UDP layer found"
+ udp = resp[UDP]
+ assert udp.sport == dport, "unexpected UDP sport: {}".format(udp.sport)
+ assert udp.dport == sport, "unexpected UDP dport: {}".format(udp.dport)
+ resp_payload = udp.payload.load
+ type_, length, magic = struct.unpack(">HHI", resp_payload[:8])
+ tid = resp_payload[8:20]
+ data = resp_payload[20:]
+ assert type_ == 0x0101, "expected type 0X0101, got 0x{:04x}".format(type_)
+ assert length == 12, "expected length 12, got {}".format(length)
+ assert (
+ magic == 0x2112A442
+ ), "expected magic 0x2112a442, got 0x{:08x}".format(magic)
+ assert (
+ tid == b"\x00" * 12
+ ), "expected tid 0x000000000000000000000000, got {:x}".format(tid)
+ expected_data = b"\x00\x01\x00\x08\x00\x01" + struct.pack(
+ ">HBBBB", sport, 192, 0, 0, 0
+ )
+ assert (
+ data == expected_data
+ ), f"unexpected data {data!r} != {expected_data!r}"
+
+
+@test
+def test_ipv6_udp_stun():
+ sports = [12345, 55555, 80, 43273]
+ dports = [80, 800, 8000, 3478]
+ payload = bytes.fromhex("000100002112a442000000000000000000000000")
+ for sport in sports:
+ for dport in dports:
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw(payload)
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(resp)
+ assert UDP in resp
+ udp = resp[UDP]
+ assert udp.sport == dport
+ assert udp.dport == sport
+ resp_payload = udp.payload.load
+ type_, length, magic = struct.unpack(">HHI", resp_payload[:8])
+ tid = resp_payload[8:20]
+ data = resp_payload[20:]
+ assert type_ == 0x0101, "expected type 0X0101, got 0x{:04x}".format(type_)
+ assert length == 24, "expected length 24, got {}".format(length)
+ assert (
+ magic == 0x2112A442
+ ), "expected magic 0x2112a442, got 0x{:08x}".format(magic)
+ assert (
+ tid == b"\x00" * 12
+ ), "expected tid 0x000000000000000000000000, got {:x}".format(tid)
+ expected_data = (
+ bytes.fromhex("000100140002")
+ + struct.pack(">H", sport)
+ + inet_pton(AF_INET6, "2001:41d0::1234:5678")
+ )
+ assert data == expected_data, "unexpected data: {}".format(data)
+
+
+@test
+def test_ipv4_udp_stun_change_port():
+ sports = [12345, 55555, 80, 43273]
+ dports = [80, 800, 8000, 3478, 65535]
+ payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002")
+ for sport in sports:
+ for dport in dports:
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw(payload)
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ip_checksum(resp)
+ assert UDP in resp, "no UDP layer found"
+ udp = resp[UDP]
+ assert (
+ udp.sport == (dport + 1) % 2**16
+ ), "expected answer from UDP/{}, got it from UDP/{}".format(
+ (dport + 1) % 2**16, udp.sport
+ )
+ assert (
+ udp.dport == sport
+ ), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport)
+ resp_payload = udp.payload.load
+ type_, length = struct.unpack(">HH", resp_payload[:4])
+ tid = resp_payload[4:20]
+ data = resp_payload[20:]
+ assert type_ == 0x0101, "expected type 0X0101, got 0x{:04x}".format(type_)
+ assert length == 12, "expected length 12, got {}".format(length)
+ assert tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c"), (
+ "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid
+ )
+ expected_data = b"\x00\x01\x00\x08\x00\x01" + struct.pack(
+ ">HBBBB", sport, 192, 0, 0, 0
+ )
+ assert (
+ data == expected_data
+ ), f"unexpected data {data!r} != {expected_data!r}"
+
+
+@test
+def test_ipv6_udp_stun_change_port():
+ sports = [12345, 55555, 80, 43273]
+ dports = [80, 800, 8000, 3478, 65535]
+ payload = bytes.fromhex("0001000803a3b9464dd8eb75e19481474293845c0003000400000002")
+ for sport in sports:
+ for dport in dports:
+ req = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / UDP(sport=sport, dport=dport)
+ / Raw(payload)
+ )
+ resp = srp1(req, timeout=1)
+ assert resp is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(resp)
+ assert UDP in resp, "expecting UDP layer in answer, got nothing"
+ udp = resp[UDP]
+ assert (
+ udp.sport == (dport + 1) % 2**16
+ ), "expected answer from UDP/{}, got it from UDP/{}".format(
+ (dport + 1) % 2**16, udp.sport
+ )
+ assert (
+ udp.dport == sport
+ ), "expected answer to UDP/{}, got it to UDP/{}".format(sport, udp.dport)
+ resp_payload = udp.payload.load
+ type_, length = struct.unpack(">HH", resp_payload[:4])
+ tid = resp_payload[4:20]
+ data = resp_payload[20:]
+ assert type_ == 0x0101, "expected type 0X0101, got 0x{:04x}".format(type_)
+ assert length == 24, "expected length 12, got {}".format(length)
+ assert tid == bytes.fromhex("03a3b9464dd8eb75e19481474293845c"), (
+ "expected tid 0x03a3b9464dd8eb75e19481474293845c, got %r" % tid
+ )
+ expected_data = (
+ bytes.fromhex("000100140002")
+ + struct.pack(">H", sport)
+ + inet_pton(AF_INET6, "2001:41d0::1234:5678")
+ )
+ assert (
+ data == expected_data
+ ), f"unexpected data {data!r} != {expected_data!r}"
diff --git a/test/src/tests/tcp.py b/test/src/tests/tcp.py
new file mode 100644
index 0000000..1eef702
--- /dev/null
+++ b/test/src/tests/tcp.py
@@ -0,0 +1,303 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from scapy.layers.inet import IP, TCP
+from scapy.layers.inet6 import IPv6
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+from scapy.sendrecv import srp1
+from scapy.volatile import RandInt
+
+from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR
+from ..core import test, check_ip_checksum, check_ipv6_checksum
+
+
+@test
+def test_ipv4_tcp_empty():
+ for p in [0, 80, 443]:
+ req = Ether(dst=MAC_ADDR) / IP(dst=IPV4_ADDR, proto=6) / Raw() # UDP
+ repl = srp1(req, timeout=1)
+ assert repl is None, "expecting no answer, got one"
+
+
+@test
+def test_ipv6_tcp_empty():
+ for p in [0, 80, 443]:
+ req = Ether(dst=MAC_ADDR) / IPv6(dst=IPV6_ADDR, nh=6) / Raw() # UDP
+ repl = srp1(req, timeout=1)
+ assert repl is None, "expecting no answer, got one"
+
+
+@test
+def test_tcp_syn():
+ ##### SYN-ACK #####
+ # test a list of ports, randomly generated once
+ ports_to_test = [
+ 1152,
+ 2003,
+ 2193,
+ 3709,
+ 4054,
+ 6605,
+ 6737,
+ 6875,
+ 7320,
+ 8898,
+ 9513,
+ 9738,
+ 10623,
+ 10723,
+ 11253,
+ 12125,
+ 12189,
+ 12873,
+ 14648,
+ 14659,
+ 16242,
+ 16243,
+ 17209,
+ 17492,
+ 17667,
+ 17838,
+ 18081,
+ 18682,
+ 18790,
+ 19124,
+ 19288,
+ 19558,
+ 19628,
+ 19789,
+ 20093,
+ 21014,
+ 21459,
+ 21740,
+ 24070,
+ 24312,
+ 24576,
+ 26939,
+ 27136,
+ 27165,
+ 27361,
+ 29971,
+ 31088,
+ 33011,
+ 33068,
+ 34990,
+ 35093,
+ 35958,
+ 36626,
+ 36789,
+ 37130,
+ 37238,
+ 37256,
+ 37697,
+ 37890,
+ 38958,
+ 42131,
+ 43864,
+ 44420,
+ 44655,
+ 44868,
+ 45157,
+ 46213,
+ 46497,
+ 46955,
+ 49049,
+ 49067,
+ 49452,
+ 49480,
+ 50498,
+ 50945,
+ 51181,
+ 52890,
+ 53301,
+ 53407,
+ 53417,
+ 53980,
+ 55827,
+ 56483,
+ 58552,
+ 58713,
+ 58836,
+ 59362,
+ 59560,
+ 60534,
+ 60555,
+ 60660,
+ 61615,
+ 62402,
+ 62533,
+ 62941,
+ 63240,
+ 63339,
+ 63616,
+ 64380,
+ 65438,
+ ]
+ for p in ports_to_test:
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="S", dport=p, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ip_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags
+ assert syn_ack.ack == seq_init + 1, "wrong TCP ack value (%r != %r)" % (
+ syn_ack.ack,
+ seq_init + 1,
+ )
+
+
+@test
+def test_ipv4_tcp_psh_ack():
+ ##### PSH-ACK #####
+ sport = 26695
+ port = 445
+ seq_init = int(RandInt())
+ # send PSH-ACK first
+ psh_ack = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="PA", sport=sport, dport=port, seq=seq_init)
+ / Raw("payload")
+ )
+ syn_ack = srp1(psh_ack, timeout=1)
+ assert syn_ack is None, "no answer expected, got one"
+ # test the anti-injection mechanism
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="S", sport=sport, dport=port, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ip_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags
+ assert syn_ack.ack == seq_init + 1, "wrong TCP ack value (%r != %r)" % (
+ syn_ack.ack,
+ seq_init + 1,
+ )
+ ack = Ether(dst=MAC_ADDR) / IP(dst=IPV4_ADDR) / TCP(flags="A", dport=port)
+ # should fail because no ack given
+ psh_ack = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags="PA", sport=sport, dport=port, ack=0, seq=seq_init + 1)
+ )
+ ack = srp1(psh_ack, timeout=1)
+ assert ack is None, "no answer expected, got one"
+ # should get an answer this time
+ psh_ack = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(
+ flags="PA", sport=sport, dport=port, ack=syn_ack.seq + 1, seq=seq_init + 1
+ )
+ )
+ ack = srp1(psh_ack, timeout=1)
+ assert ack is not None, "expecting answer, got nothing"
+ check_ip_checksum(ack)
+ assert TCP in ack, "expecting TCP, got %r" % ack.summary()
+ ack = ack[TCP]
+ assert ack.flags == "A", "expecting TCP A, got %r" % syn_ack.flags
+
+
+@test
+def test_ipv6_tcp_psh_ack():
+ ##### PSH-ACK #####
+ sport = 26695
+ port = 445
+ seq_init = int(RandInt())
+ # send PSH-ACK first
+ psh_ack = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(flags="PA", sport=sport, dport=port, seq=seq_init)
+ / Raw("payload")
+ )
+ syn_ack = srp1(psh_ack, timeout=1)
+ assert syn_ack is None, "no answer expected, got one"
+ # test the anti-injection mechanism
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(flags="S", sport=sport, dport=port, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(syn_ack)
+ assert TCP in syn_ack, "expecting TCP, got %r" % syn_ack.summary()
+ syn_ack = syn_ack[TCP]
+ assert syn_ack.flags == "SA", "expecting TCP SA, got %r" % syn_ack.flags
+ assert syn_ack.ack == seq_init + 1, "wrong TCP ack value (%r != %r)" % (
+ syn_ack.ack,
+ seq_init + 1,
+ )
+ ack = Ether(dst=MAC_ADDR) / IPv6(dst=IPV6_ADDR) / TCP(flags="A", dport=port)
+ # should fail because no ack given
+ psh_ack = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(flags="PA", sport=sport, dport=port, ack=0, seq=seq_init + 1)
+ )
+ ack = srp1(psh_ack, timeout=1)
+ assert ack is None, "no answer expected, got one"
+ # should get an answer this time
+ psh_ack = (
+ Ether(dst=MAC_ADDR)
+ / IPv6(dst=IPV6_ADDR)
+ / TCP(
+ flags="PA", sport=sport, dport=port, ack=syn_ack.seq + 1, seq=seq_init + 1
+ )
+ )
+ ack = srp1(psh_ack, timeout=1)
+ assert ack is not None, "expecting answer, got nothing"
+ check_ipv6_checksum(ack)
+ assert TCP in ack, "expecting TCP, got %r" % ack.summary()
+ ack = ack[TCP]
+ assert ack.flags == "A", "expecting TCP A, got %r" % syn_ack.flags
+
+
+@test
+def test_tcp_syn_with_flags():
+ # send a SYN packet with other TCP flags, should not be answered
+ for flags in ["SA", "SR", "SF", "SPUCE"]:
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags=flags, dport=80, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is None, "expecting no answer, got one"
+ # some should be accepted to imitate a Linux network stack
+ for flags in ["SP", "SU", "SC", "SE", "SPU", "SPC", "SPE", "SPUC", "SPUE"]:
+ seq_init = int(RandInt())
+ syn = (
+ Ether(dst=MAC_ADDR)
+ / IP(dst=IPV4_ADDR)
+ / TCP(flags=flags, dport=80, seq=seq_init)
+ )
+ syn_ack = srp1(syn, timeout=1)
+ assert syn_ack is not None, "expecting answer, got None"
diff --git a/test/src/tests/udp.py b/test/src/tests/udp.py
new file mode 100644
index 0000000..55522f5
--- /dev/null
+++ b/test/src/tests/udp.py
@@ -0,0 +1,40 @@
+# This file is part of masscanned.
+# Copyright 2021 - The IVRE project
+#
+# Masscanned is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Masscanned is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
+# License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Masscanned. If not, see .
+
+from scapy.layers.inet import IP
+from scapy.layers.inet6 import IPv6
+from scapy.layers.l2 import Ether
+from scapy.packet import Raw
+from scapy.sendrecv import srp1
+
+from ..conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR
+from ..core import test
+
+
+@test
+def test_ipv4_udp_empty():
+ for p in [0, 53, 1000]:
+ req = Ether(dst=MAC_ADDR) / IP(dst=IPV4_ADDR, proto=17) / Raw() # UDP
+ repl = srp1(req, timeout=1)
+ assert repl is None, "expecting no answer, got one"
+
+
+@test
+def test_ipv6_udp_empty():
+ for p in [0, 53, 1000]:
+ req = Ether(dst=MAC_ADDR) / IPv6(dst=IPV6_ADDR, nh=17) / Raw() # UDP
+ repl = srp1(req, timeout=1)
+ assert repl is None, "expecting no answer, got one"
diff --git a/test/test_masscanned.py b/test/test_masscanned.py
index 7384005..e3b37a6 100755
--- a/test/test_masscanned.py
+++ b/test/test_masscanned.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# This file is part of masscanned.
-# Copyright 2021 - The IVRE project
+# Copyright 2021 - 2025 - The IVRE project
#
# Masscanned is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by
@@ -16,63 +16,180 @@
# You should have received a copy of the GNU General Public License
# along with Masscanned. If not, see .
-from scapy.all import *
-from time import sleep
-from tempfile import _get_candidate_names as gen_tmp_filename
-from tempfile import gettempdir
-import subprocess
-import logging
-import sys
+import atexit
+import functools
import os
+from signal import SIGINT
+import subprocess
+import sys
+from time import sleep
+from tempfile import NamedTemporaryFile
+
+try:
+ from ivre.config import guess_prefix
+except ImportError:
+ HAS_IVRE = False
+else:
+ HAS_IVRE = True
+from scapy.config import conf
+from scapy.interfaces import resolve_iface
from src.all import test_all
-from src.conf import *
+from src.conf import IPV4_ADDR, IPV6_ADDR, MAC_ADDR, OUTDIR
-# if args in CLI, they are passed to masscanned
-if len(sys.argv) > 1:
- args = " ".join(sys.argv[1:])
+
+def cleanup_net(iface):
+ subprocess.check_call(["ip", "link", "delete", iface])
+ subprocess.check_call(
+ [
+ "iptables",
+ "-D",
+ "INPUT",
+ "-i",
+ iface,
+ "-m",
+ "state",
+ "--state",
+ "ESTABLISHED",
+ "-j",
+ "ACCEPT",
+ ]
+ )
+ subprocess.check_call(["iptables", "-D", "INPUT", "-i", iface, "-j", "DROP"])
+ try:
+ os.unlink(ipfile.name)
+ except NameError:
+ pass
+
+
+def setup_net(iface):
+ # create the interfaces pair
+ atexit.register(functools.partial(cleanup_net, f"{iface}a"))
+ subprocess.check_call(
+ ["ip", "link", "add", f"{iface}a", "type", "veth", "peer", f"{iface}b"]
+ )
+ for sub in "a", "b":
+ subprocess.check_call(["ip", "link", "set", f"{iface}{sub}", "up"])
+ subprocess.check_call(["ip", "addr", "add", "dev", f"{iface}a", "192.0.0.0/31"])
+ subprocess.check_call(
+ ["ip", "addr", "add", "dev", f"{iface}a", "2001:41d0::1234:5678/96"]
+ )
+ subprocess.check_call(["ip", "route", "add", "1.2.3.4/32", "via", IPV4_ADDR])
+ # prevent problems between raw scanners (Scapy, Nmap, Masscan) and
+ # the host IP stack
+ subprocess.check_call(
+ [
+ "iptables",
+ "-A",
+ "INPUT",
+ "-i",
+ f"{iface}a",
+ "-m",
+ "state",
+ "--state",
+ "ESTABLISHED",
+ "-j",
+ "ACCEPT",
+ ]
+ )
+ subprocess.check_call(["iptables", "-A", "INPUT", "-i", f"{iface}a", "-j", "DROP"])
+ conf.route.resync()
+ conf.route6.resync()
+
+
+IFACE = "masscanned"
+setup_net(IFACE)
+TCPDUMP = bool(os.environ.get("USE_TCPDUMP"))
+if HAS_IVRE:
+ ZEEK_PASSIVERECON = bool(os.environ.get("USE_ZEEK"))
else:
- args = ""
-
-fmt = logging.Formatter("%(levelname)s\t%(message)s")
-ch = logging.StreamHandler()
-ch.setFormatter(fmt)
-ch.setLevel(logging.INFO)
-LOG = logging.getLogger(__name__)
-LOG.setLevel(logging.INFO)
-LOG.addHandler(ch)
-
-conf.iface = 'tap0'
+ ZEEK_PASSIVERECON = False
+P0F = bool(os.environ.get("USE_P0F"))
conf.verb = 0
# prepare configuration file for masscanned
-ipfile = os.path.join(gettempdir(), next(gen_tmp_filename()))
-with open(ipfile, "w") as f:
- f.write("{}\n".format(IPV4_ADDR))
- f.write("{}\n".format(IPV6_ADDR))
+with NamedTemporaryFile(delete=False, mode="w") as ipfile:
+ ipfile.write(f"{IPV4_ADDR}\n")
+ ipfile.write(f"{IPV6_ADDR}\n")
# create test interface
-tap = TunTapInterface(resolve_iface(conf.iface))
-
-# set interface
-subprocess.run("ip a a dev {} 192.0.0.2".format(conf.iface), shell=True)
-subprocess.run("ip link set {} up".format(conf.iface), shell=True)
+conf.iface = resolve_iface(f"{IFACE}a")
# start capture
-tcpdump = subprocess.Popen("tcpdump -enli {} -w {}".format(conf.iface, os.path.join(OUTDIR, "test_capture.pcap")), shell=True,
- stdin=None, stdout=None, stderr=None, close_fds=True)
+if TCPDUMP:
+ tcpdump = subprocess.Popen(
+ [
+ "tcpdump",
+ "-enli",
+ f"{IFACE}a",
+ "-w",
+ os.path.join(OUTDIR, "test_capture.pcap"),
+ ]
+ )
+if ZEEK_PASSIVERECON:
+ zeek = subprocess.Popen(
+ [
+ "zeek",
+ "-C",
+ "-b",
+ "-i",
+ f"{IFACE}a",
+ os.path.join(
+ guess_prefix("zeek"),
+ "ivre",
+ "passiverecon",
+ "bare.zeek",
+ ),
+ "-e",
+ "redef tcp_content_deliver_all_resp = T; "
+ "redef tcp_content_deliver_all_orig = T; "
+ f"redef PassiveRecon::HONEYPOTS += {{ {IPV4_ADDR}, [{IPV6_ADDR}] }}",
+ ],
+ stdout=open(os.path.join(OUTDIR, "zeek_passiverecon.stdout"), "w"),
+ stderr=open(os.path.join(OUTDIR, "zeek_passiverecon.stderr"), "w"),
+ )
+if P0F:
+ p0f = subprocess.Popen(
+ ["p0f", "-i", f"{IFACE}a", "-o", os.path.join(OUTDIR, "p0f_log.txt")],
+ stdout=open(os.path.join(OUTDIR, "p0f.stdout"), "w"),
+ stderr=open(os.path.join(OUTDIR, "p0f.stderr"), "w"),
+ )
# run masscanned
-masscanned = subprocess.Popen("RUST_BACKTRACE=1 ./target/debug/masscanned -vvvvv -i {} -f {} -a {} {}".format(conf.iface, ipfile, MAC_ADDR, args), shell=True,
- stdin=None, stdout=open("test/res/masscanned.stdout", "w"), stderr=open("test/res/masscanned.stderr", "w"), close_fds=True)
+masscanned = subprocess.Popen(
+ [
+ "./target/debug/masscanned",
+ "-vvvvv",
+ "-i",
+ f"{IFACE}b",
+ "--self-ip-file",
+ ipfile.name,
+ "-m",
+ MAC_ADDR,
+ ]
+ # if args in CLI, they are passed to masscanned
+ + sys.argv[1:],
+ env=dict(os.environ, RUST_BACKTRACE="1"),
+ stdout=open(os.path.join(OUTDIR, "masscanned.stdout"), "w"),
+ stderr=open(os.path.join(OUTDIR, "masscanned.stderr"), "w"),
+)
sleep(1)
try:
- test_all(tap)
+ result = test_all(masscanned)
except AssertionError:
- pass
+ result = -1
# terminate masscanned
-masscanned.kill()
+masscanned.send_signal(SIGINT)
+masscanned.wait()
# terminate capture
-sleep(2)
-tcpdump.kill()
+if TCPDUMP:
+ tcpdump.send_signal(SIGINT)
+ tcpdump.wait()
+if ZEEK_PASSIVERECON:
+ zeek.send_signal(SIGINT)
+ zeek.wait()
+if P0F:
+ p0f.send_signal(SIGINT)
+ p0f.wait()
+sys.exit(result)