Added scaffoded project
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
target
|
||||||
|
.env
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
DATABASE_URL="postgres://camfinder:camfinder@127.0.0.1:5432/camfinder"
|
||||||
|
SCAN_CIDRS="192.168.15.0/24"
|
||||||
|
SCAN_CIDR_FILE=""
|
||||||
|
SCAN_PORT=554
|
||||||
|
SCAN_TIMEOUT_MS=1200
|
||||||
|
SCAN_CONCURRENCY=16
|
||||||
|
SCAN_CHUNK_SIZE=64
|
||||||
|
SCAN_MODE="local"
|
||||||
|
COORDINATOR_BIND="0.0.0.0:666"
|
||||||
|
COORDINATOR_ADDR="127.0.0.1:666"
|
||||||
|
WORKER_NAME="worker-1"
|
||||||
Generated
+803
@@ -0,0 +1,803 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "2.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "block-buffer"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bytes"
|
||||||
|
version = "1.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "camfinder"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"dotenvy",
|
||||||
|
"tokio",
|
||||||
|
"tokio-postgres",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chacha20"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmov"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-oid"
|
||||||
|
version = "0.10.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpufeatures"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-common"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
||||||
|
dependencies = [
|
||||||
|
"hybrid-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctutils"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
||||||
|
dependencies = [
|
||||||
|
"cmov",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "digest"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
|
dependencies = [
|
||||||
|
"block-buffer",
|
||||||
|
"const-oid",
|
||||||
|
"crypto-common",
|
||||||
|
"ctutils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenvy"
|
||||||
|
version = "0.15.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "errno"
|
||||||
|
version = "0.3.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-channel"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hybrid-array"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
|
||||||
|
dependencies = [
|
||||||
|
"typenum",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.186"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libredox"
|
||||||
|
version = "0.1.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lock_api"
|
||||||
|
version = "0.4.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||||
|
dependencies = [
|
||||||
|
"scopeguard",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "md-5"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-foundation"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-system-configuration"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396"
|
||||||
|
dependencies = [
|
||||||
|
"objc2-core-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.21.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||||
|
dependencies = [
|
||||||
|
"lock_api",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking_lot_core"
|
||||||
|
version = "0.9.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"redox_syscall",
|
||||||
|
"smallvec",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "percent-encoding"
|
||||||
|
version = "2.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||||
|
dependencies = [
|
||||||
|
"phf_shared",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "postgres-protocol"
|
||||||
|
version = "0.6.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"fallible-iterator",
|
||||||
|
"hmac",
|
||||||
|
"md-5",
|
||||||
|
"memchr",
|
||||||
|
"rand",
|
||||||
|
"sha2",
|
||||||
|
"stringprep",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "postgres-types"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"fallible-iterator",
|
||||||
|
"postgres-protocol",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.106"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.46"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "6.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||||
|
dependencies = [
|
||||||
|
"chacha20",
|
||||||
|
"getrandom",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redox_syscall"
|
||||||
|
version = "0.5.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scopeguard"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha2"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||||
|
dependencies = [
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smallvec"
|
||||||
|
version = "1.15.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stringprep"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-bidi",
|
||||||
|
"unicode-normalization",
|
||||||
|
"unicode-properties",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.118"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec_macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.52.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-postgres"
|
||||||
|
version = "0.7.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"fallible-iterator",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"parking_lot",
|
||||||
|
"percent-encoding",
|
||||||
|
"phf",
|
||||||
|
"pin-project-lite",
|
||||||
|
"postgres-protocol",
|
||||||
|
"postgres-types",
|
||||||
|
"rand",
|
||||||
|
"socket2",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"whoami",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typenum"
|
||||||
|
version = "1.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-bidi"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-normalization"
|
||||||
|
version = "0.1.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-properties"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.14.7+wasi-0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
|
||||||
|
dependencies = [
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.4+wasi-0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasite"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42"
|
||||||
|
dependencies = [
|
||||||
|
"wasi 0.14.7+wasi-0.2.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.126"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "whoami"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"libredox",
|
||||||
|
"objc2-system-configuration",
|
||||||
|
"wasite",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-link"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.61.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.57.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "camfinder"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
dotenvy = "0.15"
|
||||||
|
tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "signal", "time"] }
|
||||||
|
tokio-postgres = "0.7"
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
FROM rust:1.78-bookworm AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/target/release/camfinder /usr/local/bin/camfinder
|
||||||
|
|
||||||
|
ENTRYPOINT ["camfinder"]
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# CamFinder
|
||||||
|
|
||||||
|
Scanner TCP em Rust para descobrir hosts em CIDRs internos, testar a porta `554` e salvar progresso e endpoints abertos em PostgreSQL.
|
||||||
|
|
||||||
|
O projeto tem três modos:
|
||||||
|
|
||||||
|
- `local`: roda o scan inteiro no processo atual.
|
||||||
|
- `coordinator`: recebe conexões de workers, distribui blocos e grava resultados no banco.
|
||||||
|
- `worker`: conecta ao coordenador, recebe blocos e testa os hosts.
|
||||||
|
|
||||||
|
O progresso é salvo por bloco, então o scan pode ser retomado sem repetir o que já foi concluído.
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
1. Copie `.env.example` para `.env`.
|
||||||
|
2. Configure `DATABASE_URL` apontando para um Postgres acessível.
|
||||||
|
3. Rode `cargo run -- local`, `cargo run -- coordinator` ou `cargo run -- worker`.
|
||||||
|
|
||||||
|
## Docker Compose
|
||||||
|
|
||||||
|
O `docker-compose.yml` já sobe:
|
||||||
|
|
||||||
|
- `postgres` para persistência.
|
||||||
|
- `coordinator` exposto no host pela porta `666`.
|
||||||
|
- `worker` conectado ao coordenador na rede interna do compose.
|
||||||
|
|
||||||
|
Para escalar workers:
|
||||||
|
|
||||||
|
- `docker compose up --scale worker=3`
|
||||||
|
|
||||||
|
Para workers de outras máquinas, use `vps.valmo.dev:666` como endereço do coordenador.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
- `cargo build`
|
||||||
|
- `cargo run`
|
||||||
|
- `npm run scan`
|
||||||
|
- `npm run coord`
|
||||||
|
- `npm run worker`
|
||||||
|
|
||||||
|
## Variáveis úteis
|
||||||
|
|
||||||
|
- `DATABASE_URL`
|
||||||
|
- `SCAN_CIDRS`
|
||||||
|
- `SCAN_CIDR`
|
||||||
|
- `SCAN_CIDR_FILE`
|
||||||
|
- `SCAN_CONCURRENCY`
|
||||||
|
- `SCAN_CHUNK_SIZE`
|
||||||
|
- `SCAN_TIMEOUT_MS`
|
||||||
|
- `SCAN_MODE`
|
||||||
|
- `COORDINATOR_BIND`
|
||||||
|
- `COORDINATOR_ADDR`
|
||||||
|
- `WORKER_NAME`
|
||||||
|
|
||||||
|
## Restrições
|
||||||
|
|
||||||
|
O projeto aceita somente redes privadas `10.0.0.0/8`, `172.16.0.0/12` e `192.168.0.0/16`.
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: camfinder
|
||||||
|
POSTGRES_USER: camfinder
|
||||||
|
POSTGRES_PASSWORD: camfinder
|
||||||
|
volumes:
|
||||||
|
- camfinder-postgres:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U camfinder -d camfinder"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 5s
|
||||||
|
|
||||||
|
coordinator:
|
||||||
|
build: .
|
||||||
|
command: ["coordinator"]
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://camfinder:camfinder@postgres:5432/camfinder
|
||||||
|
SCAN_CIDRS: 192.168.15.0/24
|
||||||
|
SCAN_PORT: 554
|
||||||
|
SCAN_TIMEOUT_MS: 1200
|
||||||
|
SCAN_CONCURRENCY: 16
|
||||||
|
SCAN_CHUNK_SIZE: 64
|
||||||
|
COORDINATOR_BIND: 0.0.0.0:666
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "666:666"
|
||||||
|
|
||||||
|
worker:
|
||||||
|
build: .
|
||||||
|
command: ["worker"]
|
||||||
|
environment:
|
||||||
|
COORDINATOR_ADDR: coordinator:666
|
||||||
|
SCAN_PORT: 554
|
||||||
|
SCAN_TIMEOUT_MS: 1200
|
||||||
|
SCAN_CONCURRENCY: 16
|
||||||
|
WORKER_NAME: worker
|
||||||
|
depends_on:
|
||||||
|
coordinator:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
camfinder-postgres:
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Faixas IPv4
|
||||||
|
|
||||||
|
| Faixa IPv4 | Intervalo completo | Nome | Roteável na internet? | Observação |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `0.0.0.0/8` | `0.0.0.0` até `0.255.255.255` | Especial | Não | Endereço não especificado e usos reservados |
|
||||||
|
| `1.0.0.0/8` até `9.255.255.255/8` | `1.0.0.0` até `9.255.255.255` | Público | Sim | Inclui IPs como `1.2.3.4` |
|
||||||
|
| `10.0.0.0/8` | `10.0.0.0` até `10.255.255.255` | Privado | Não | Redes internas grandes |
|
||||||
|
| `11.0.0.0/8` até `100.63.255.255` | `11.0.0.0` até `100.63.255.255` | Público | Sim | Faixa pública, exceto blocos reservados menores |
|
||||||
|
| `100.64.0.0/10` | `100.64.0.0` até `100.127.255.255` | CGNAT | Não | Compartilhado por operadoras |
|
||||||
|
| `100.128.0.0/9` até `126.255.255.255` | `100.128.0.0` até `126.255.255.255` | Público | Sim | Faixa pública, exceto blocos reservados menores |
|
||||||
|
| `127.0.0.0/8` | `127.0.0.0` até `127.255.255.255` | Loopback | Não | A própria máquina |
|
||||||
|
| `128.0.0.0/2` com exceções | `128.0.0.0` até `223.255.255.255` | Público | Sim | Faixa pública geral, com alguns blocos especiais reservados dentro dela |
|
||||||
|
| `169.254.0.0/16` | `169.254.0.0` até `169.254.255.255` | Link-local | Não | IP automático sem DHCP |
|
||||||
|
| `172.16.0.0/12` | `172.16.0.0` até `172.31.255.255` | Privado | Não | Redes internas médias |
|
||||||
|
| `192.0.0.0/24` e blocos próximos | `192.0.0.0` até `192.0.0.255` | Reservado | Não | Usos especiais diversos |
|
||||||
|
| `192.0.2.0/24` | `192.0.2.0` até `192.0.2.255` | Documentação | Não | Exemplos e manuais |
|
||||||
|
| `192.88.99.0/24` | `192.88.99.0` até `192.88.99.255` | Reservado | Não | Uso especial histórico |
|
||||||
|
| `192.168.0.0/16` | `192.168.0.0` até `192.168.255.255` | Privado | Não | Casas, escritórios, laboratórios |
|
||||||
|
| `198.18.0.0/15` | `198.18.0.0` até `198.19.255.255` | Teste | Não | Benchmarks e testes de rede |
|
||||||
|
| `198.51.100.0/24` | `198.51.100.0` até `198.51.100.255` | Documentação | Não | Exemplos e manuais |
|
||||||
|
| `203.0.113.0/24` | `203.0.113.0` até `203.0.113.255` | Documentação | Não | Exemplos e manuais |
|
||||||
|
| `224.0.0.0/4` | `224.0.0.0` até `239.255.255.255` | Multicast | Não | Um envio para vários destinos |
|
||||||
|
| `240.0.0.0/4` | `240.0.0.0` até `255.255.255.254` | Reservado | Não | Reservado para uso futuro e especial |
|
||||||
|
| `255.255.255.255` | `255.255.255.255` | Broadcast limitado | Não | Todos os hosts da rede local |
|
||||||
|
|
||||||
|
## Leitura prática
|
||||||
|
|
||||||
|
- `1.2.3.4` entra na faixa pública roteável.
|
||||||
|
- `192.168.15.8` entra na faixa privada e não é roteável na internet.
|
||||||
|
- `127.0.0.1` é loopback.
|
||||||
|
- `100.72.4.1` é CGNAT, normalmente não é roteável diretamente.
|
||||||
+352
@@ -0,0 +1,352 @@
|
|||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::{
|
||||||
|
env, fs,
|
||||||
|
net::SocketAddr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum Mode {
|
||||||
|
Local,
|
||||||
|
Coordinator,
|
||||||
|
Worker,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub mode: Mode,
|
||||||
|
pub cidrs: Vec<String>,
|
||||||
|
pub port: u16,
|
||||||
|
pub timeout_ms: u64,
|
||||||
|
pub concurrency: usize,
|
||||||
|
pub chunk_size: usize,
|
||||||
|
pub database_url: String,
|
||||||
|
pub coordinator_bind: SocketAddr,
|
||||||
|
pub coordinator_addr: SocketAddr,
|
||||||
|
pub worker_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn from_env_and_args() -> Result<Self> {
|
||||||
|
let mut args = env::args().skip(1).collect::<Vec<_>>();
|
||||||
|
let mode = parse_mode(&mut args)?;
|
||||||
|
let cli = CliArgs::from_args(&args)?;
|
||||||
|
|
||||||
|
let cidrs = load_cidrs(cli.cidr_file.as_deref(), cli.cidrs.as_deref())?;
|
||||||
|
Ok(Self {
|
||||||
|
mode,
|
||||||
|
cidrs,
|
||||||
|
port: cli
|
||||||
|
.port
|
||||||
|
.or_else(|| parse_u16("SCAN_PORT", 554).ok())
|
||||||
|
.unwrap_or(554),
|
||||||
|
timeout_ms: cli
|
||||||
|
.timeout_ms
|
||||||
|
.or_else(|| parse_u64("SCAN_TIMEOUT_MS", 1200).ok())
|
||||||
|
.unwrap_or(1200),
|
||||||
|
concurrency: cli
|
||||||
|
.concurrency
|
||||||
|
.or_else(|| parse_usize("SCAN_CONCURRENCY", default_concurrency()).ok())
|
||||||
|
.unwrap_or_else(default_concurrency),
|
||||||
|
chunk_size: cli
|
||||||
|
.chunk_size
|
||||||
|
.or_else(|| parse_usize("SCAN_CHUNK_SIZE", default_chunk_size()).ok())
|
||||||
|
.unwrap_or_else(default_chunk_size),
|
||||||
|
database_url: cli.database_url.unwrap_or_else(database_url_from_env),
|
||||||
|
coordinator_bind: cli
|
||||||
|
.coordinator_bind
|
||||||
|
.or_else(|| parse_socket_addr("COORDINATOR_BIND", "0.0.0.0:666").ok())
|
||||||
|
.unwrap_or_else(|| "0.0.0.0:666".parse().expect("valid default bind address")),
|
||||||
|
coordinator_addr: cli
|
||||||
|
.coordinator_addr
|
||||||
|
.or_else(|| parse_socket_addr("COORDINATOR_ADDR", "127.0.0.1:666").ok())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
"127.0.0.1:666"
|
||||||
|
.parse()
|
||||||
|
.expect("valid default coordinator address")
|
||||||
|
}),
|
||||||
|
worker_name: cli.worker_name.unwrap_or_else(default_worker_name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct CliArgs {
|
||||||
|
cidrs: Option<String>,
|
||||||
|
cidr_file: Option<PathBuf>,
|
||||||
|
database_url: Option<String>,
|
||||||
|
coordinator_bind: Option<SocketAddr>,
|
||||||
|
coordinator_addr: Option<SocketAddr>,
|
||||||
|
worker_name: Option<String>,
|
||||||
|
port: Option<u16>,
|
||||||
|
timeout_ms: Option<u64>,
|
||||||
|
concurrency: Option<usize>,
|
||||||
|
chunk_size: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CliArgs {
|
||||||
|
fn from_args(args: &[String]) -> Result<Self> {
|
||||||
|
let mut cli = Self::default();
|
||||||
|
let mut index = 0;
|
||||||
|
|
||||||
|
while index < args.len() {
|
||||||
|
let arg = &args[index];
|
||||||
|
match arg.as_str() {
|
||||||
|
"--cidrs" => {
|
||||||
|
cli.cidrs = Some(next_value(args, &mut index, "--cidrs")?);
|
||||||
|
}
|
||||||
|
"--cidr-file" => {
|
||||||
|
cli.cidr_file =
|
||||||
|
Some(PathBuf::from(next_value(args, &mut index, "--cidr-file")?));
|
||||||
|
}
|
||||||
|
"--database" | "--database-url" => {
|
||||||
|
cli.database_url = Some(next_value(args, &mut index, arg)?);
|
||||||
|
}
|
||||||
|
"--bind" => {
|
||||||
|
cli.coordinator_bind = Some(parse_socket_addr_literal(&next_value(
|
||||||
|
args, &mut index, "--bind",
|
||||||
|
)?)?);
|
||||||
|
}
|
||||||
|
"--connect" => {
|
||||||
|
cli.coordinator_addr = Some(parse_socket_addr_literal(&next_value(
|
||||||
|
args,
|
||||||
|
&mut index,
|
||||||
|
"--connect",
|
||||||
|
)?)?);
|
||||||
|
}
|
||||||
|
"--name" => {
|
||||||
|
cli.worker_name = Some(next_value(args, &mut index, "--name")?);
|
||||||
|
}
|
||||||
|
"--port" => {
|
||||||
|
cli.port = Some(parse_u16_literal(&next_value(args, &mut index, "--port")?)?);
|
||||||
|
}
|
||||||
|
"--timeout-ms" => {
|
||||||
|
cli.timeout_ms = Some(parse_u64_literal(&next_value(
|
||||||
|
args,
|
||||||
|
&mut index,
|
||||||
|
"--timeout-ms",
|
||||||
|
)?)?);
|
||||||
|
}
|
||||||
|
"--concurrency" => {
|
||||||
|
cli.concurrency = Some(parse_usize_literal(&next_value(
|
||||||
|
args,
|
||||||
|
&mut index,
|
||||||
|
"--concurrency",
|
||||||
|
)?)?);
|
||||||
|
}
|
||||||
|
"--chunk-size" => {
|
||||||
|
cli.chunk_size = Some(parse_usize_literal(&next_value(
|
||||||
|
args,
|
||||||
|
&mut index,
|
||||||
|
"--chunk-size",
|
||||||
|
)?)?);
|
||||||
|
}
|
||||||
|
_ if arg.starts_with("--") => {
|
||||||
|
return Err(anyhow!("unknown argument: {arg}"));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(anyhow!("unexpected positional argument: {arg}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(cli)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_mode(args: &mut Vec<String>) -> Result<Mode> {
|
||||||
|
if let Some(first) = args.first() {
|
||||||
|
if !first.starts_with('-') {
|
||||||
|
let mode = parse_mode_literal(first)?;
|
||||||
|
args.remove(0);
|
||||||
|
return Ok(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match env::var("SCAN_MODE") {
|
||||||
|
Ok(value) => parse_mode_literal(value.trim()),
|
||||||
|
Err(_) => Ok(Mode::Local),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_mode_literal(value: &str) -> Result<Mode> {
|
||||||
|
match value.trim().to_ascii_lowercase().as_str() {
|
||||||
|
"local" | "scan" => Ok(Mode::Local),
|
||||||
|
"coordinator" | "coord" | "server" => Ok(Mode::Coordinator),
|
||||||
|
"worker" => Ok(Mode::Worker),
|
||||||
|
other => Err(anyhow!(
|
||||||
|
"modo inválido: {other}. Use local, coordinator ou worker"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_cidrs(file: Option<&Path>, inline: Option<&str>) -> Result<Vec<String>> {
|
||||||
|
if let Some(inline) = inline {
|
||||||
|
let cidrs = parse_cidr_list(inline);
|
||||||
|
if !cidrs.is_empty() {
|
||||||
|
return Ok(cidrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(file) = file {
|
||||||
|
let content = fs::read_to_string(file)
|
||||||
|
.with_context(|| format!("failed to read CIDR file {}", file.display()))?;
|
||||||
|
let cidrs = parse_cidr_list(&content);
|
||||||
|
if !cidrs.is_empty() {
|
||||||
|
return Ok(cidrs);
|
||||||
|
}
|
||||||
|
return Err(anyhow!("arquivo de CIDRs vazio: {}", file.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(value) = env::var("SCAN_CIDRS") {
|
||||||
|
let value = value.trim();
|
||||||
|
if !value.is_empty() {
|
||||||
|
let cidrs = parse_cidr_list(value);
|
||||||
|
if !cidrs.is_empty() {
|
||||||
|
return Ok(cidrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(path) = env::var("SCAN_CIDR_FILE") {
|
||||||
|
let path = path.trim();
|
||||||
|
if !path.is_empty() {
|
||||||
|
let content = fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("failed to read CIDR file {path}"))?;
|
||||||
|
let cidrs = parse_cidr_list(&content);
|
||||||
|
if !cidrs.is_empty() {
|
||||||
|
return Ok(cidrs);
|
||||||
|
}
|
||||||
|
return Err(anyhow!("arquivo de CIDRs vazio: {path}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(value) = env::var("SCAN_CIDR") {
|
||||||
|
let value = value.trim();
|
||||||
|
if !value.is_empty() {
|
||||||
|
let cidrs = parse_cidr_list(value);
|
||||||
|
if !cidrs.is_empty() {
|
||||||
|
return Ok(cidrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_cidr_list(input: &str) -> Vec<String> {
|
||||||
|
input
|
||||||
|
.lines()
|
||||||
|
.flat_map(|line| line.split('#').next())
|
||||||
|
.flat_map(|line| line.split(|c: char| c == ',' || c.is_whitespace()))
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|entry| !entry.is_empty())
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn database_url_from_env() -> String {
|
||||||
|
let value = read_env("DATABASE_URL", "");
|
||||||
|
if value.is_empty() {
|
||||||
|
"postgres://postgres:postgres@127.0.0.1:5432/camfinder".to_string()
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_worker_name() -> String {
|
||||||
|
env::var("WORKER_NAME")
|
||||||
|
.ok()
|
||||||
|
.map(|value| value.trim().to_string())
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let host = env::var("HOSTNAME").unwrap_or_else(|_| "worker".to_string());
|
||||||
|
format!("{}-{}", host.trim(), std::process::id())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_env(name: &str, fallback: &str) -> String {
|
||||||
|
env::var(name)
|
||||||
|
.map(|value| value.trim().to_string())
|
||||||
|
.unwrap_or_else(|_| fallback.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_socket_addr(name: &str, fallback: &str) -> Result<SocketAddr> {
|
||||||
|
parse_socket_addr_literal(&read_env(name, fallback))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_socket_addr_literal(value: &str) -> Result<SocketAddr> {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| anyhow!("invalid socket address: {value}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u16(name: &str, fallback: u16) -> Result<u16> {
|
||||||
|
parse_number(name, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u64(name: &str, fallback: u64) -> Result<u64> {
|
||||||
|
parse_number(name, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_usize(name: &str, fallback: usize) -> Result<usize> {
|
||||||
|
parse_number(name, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u16_literal(value: &str) -> Result<u16> {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| anyhow!("invalid u16 value: {value}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_u64_literal(value: &str) -> Result<u64> {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| anyhow!("invalid u64 value: {value}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_usize_literal(value: &str) -> Result<usize> {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| anyhow!("invalid usize value: {value}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_value(args: &[String], index: &mut usize, flag: &str) -> Result<String> {
|
||||||
|
let value_index = *index + 1;
|
||||||
|
let value = args
|
||||||
|
.get(value_index)
|
||||||
|
.ok_or_else(|| anyhow!("missing value for {flag}"))?
|
||||||
|
.clone();
|
||||||
|
*index = value_index;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_concurrency() -> usize {
|
||||||
|
let cpus = std::thread::available_parallelism()
|
||||||
|
.map(|n| n.get())
|
||||||
|
.unwrap_or(4);
|
||||||
|
(cpus.saturating_mul(2)).clamp(4, 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_chunk_size() -> usize {
|
||||||
|
default_concurrency().saturating_mul(2).clamp(32, 256)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_number<T>(name: &str, fallback: T) -> Result<T>
|
||||||
|
where
|
||||||
|
T: std::str::FromStr + Copy,
|
||||||
|
{
|
||||||
|
match env::var(name) {
|
||||||
|
Ok(value) => value
|
||||||
|
.trim()
|
||||||
|
.parse::<T>()
|
||||||
|
.map_err(|_| anyhow!("invalid value for {name}: {value}")),
|
||||||
|
Err(_) => Ok(fallback),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,398 @@
|
|||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
db::Database,
|
||||||
|
models::{ChunkRecord, PendingRange},
|
||||||
|
scan::{build_pending_ranges, load_targets},
|
||||||
|
terminal::{ScreenState, TerminalUi},
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
net::Ipv4Addr,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
|
Arc, Mutex,
|
||||||
|
},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||||
|
net::{TcpListener, TcpStream},
|
||||||
|
task::JoinSet,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn run_coordinator(
|
||||||
|
config: Config,
|
||||||
|
database: Arc<Database>,
|
||||||
|
ui: Arc<TerminalUi>,
|
||||||
|
interrupted: Arc<AtomicBool>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let targets = Arc::new(load_targets(&config.cidrs)?);
|
||||||
|
let target_count = targets.len();
|
||||||
|
let chunk_records = database.load_chunk_records().await?;
|
||||||
|
let (tested_before_run, _open_before_run) = database.summarize_chunks().await?;
|
||||||
|
let pending_ranges = build_pending_ranges(target_count, config.chunk_size, &chunk_records);
|
||||||
|
let pending_count: usize = pending_ranges.iter().map(|range| range.len()).sum();
|
||||||
|
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.print_banner();
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: if pending_count > 0 {
|
||||||
|
targets[pending_ranges[0].start].to_string()
|
||||||
|
} else {
|
||||||
|
"concluído".to_string()
|
||||||
|
},
|
||||||
|
tested: tested_before_run,
|
||||||
|
total: target_count,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"Coordinator ready on {} for {} hosts across {} CIDRs. Pending {}, already tested {}.",
|
||||||
|
config.coordinator_bind,
|
||||||
|
target_count,
|
||||||
|
config.cidrs.len(),
|
||||||
|
pending_count,
|
||||||
|
tested_before_run
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending_ranges.is_empty() {
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: "concluído".to_string(),
|
||||||
|
tested: target_count,
|
||||||
|
total: target_count,
|
||||||
|
});
|
||||||
|
ui.finish();
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let listener = TcpListener::bind(config.coordinator_bind)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to bind {}", config.coordinator_bind))?;
|
||||||
|
let queue = Arc::new(Mutex::new(VecDeque::from(pending_ranges)));
|
||||||
|
let tested_this_run = Arc::new(AtomicUsize::new(0));
|
||||||
|
let open_this_run = Arc::new(AtomicUsize::new(0));
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut handlers = JoinSet::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if interrupted.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
accept_result = listener.accept() => {
|
||||||
|
let (stream, peer) = accept_result?;
|
||||||
|
let queue = Arc::clone(&queue);
|
||||||
|
let targets = Arc::clone(&targets);
|
||||||
|
let database = Arc::clone(&database);
|
||||||
|
let ui = Arc::clone(&ui);
|
||||||
|
let interrupted = Arc::clone(&interrupted);
|
||||||
|
let tested_this_run = Arc::clone(&tested_this_run);
|
||||||
|
let open_this_run = Arc::clone(&open_this_run);
|
||||||
|
let config = config.clone();
|
||||||
|
|
||||||
|
handlers.spawn(async move {
|
||||||
|
handle_worker(
|
||||||
|
stream,
|
||||||
|
peer,
|
||||||
|
queue,
|
||||||
|
targets,
|
||||||
|
database,
|
||||||
|
ui,
|
||||||
|
interrupted,
|
||||||
|
tested_before_run,
|
||||||
|
tested_this_run,
|
||||||
|
open_this_run,
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
}
|
||||||
|
maybe_finished = handlers.join_next(), if !handlers.is_empty() => {
|
||||||
|
if let Some(join_result) = maybe_finished {
|
||||||
|
join_result??;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if queue.lock().expect("queue mutex poisoned").is_empty() && handlers.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(join_result) = handlers.join_next().await {
|
||||||
|
join_result??;
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = start.elapsed().as_secs_f64();
|
||||||
|
let total_open_count = database.count_open_endpoints(config.port).await?;
|
||||||
|
|
||||||
|
if interrupted.load(Ordering::SeqCst) {
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: "interrompido".to_string(),
|
||||||
|
tested: tested_before_run + tested_this_run.load(Ordering::SeqCst),
|
||||||
|
total: target_count,
|
||||||
|
});
|
||||||
|
ui.finish();
|
||||||
|
} else {
|
||||||
|
println!("Interrupted after {:.2}s.", duration);
|
||||||
|
println!(
|
||||||
|
"Hosts tested in this run: {}",
|
||||||
|
tested_this_run.load(Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Open endpoints found: {}",
|
||||||
|
open_this_run.load(Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: "concluído".to_string(),
|
||||||
|
tested: target_count,
|
||||||
|
total: target_count,
|
||||||
|
});
|
||||||
|
ui.finish();
|
||||||
|
} else {
|
||||||
|
println!("Coordinator finished in {:.2}s", duration);
|
||||||
|
println!(
|
||||||
|
"Hosts tested in this run: {}",
|
||||||
|
tested_this_run.load(Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Open endpoints found in this run: {}",
|
||||||
|
open_this_run.load(Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
println!("Total open endpoints in DB: {}", total_open_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_worker(
|
||||||
|
stream: TcpStream,
|
||||||
|
peer: std::net::SocketAddr,
|
||||||
|
queue: Arc<Mutex<VecDeque<PendingRange>>>,
|
||||||
|
targets: Arc<Vec<Ipv4Addr>>,
|
||||||
|
database: Arc<Database>,
|
||||||
|
ui: Arc<TerminalUi>,
|
||||||
|
interrupted: Arc<AtomicBool>,
|
||||||
|
tested_before_run: usize,
|
||||||
|
tested_this_run: Arc<AtomicUsize>,
|
||||||
|
open_this_run: Arc<AtomicUsize>,
|
||||||
|
config: Config,
|
||||||
|
) -> Result<()> {
|
||||||
|
let (read_half, mut write_half) = stream.into_split();
|
||||||
|
let mut lines = BufReader::new(read_half).lines();
|
||||||
|
|
||||||
|
let worker_name = match lines.next_line().await? {
|
||||||
|
Some(line) if line.starts_with("HELLO ") => line[6..].trim().to_string(),
|
||||||
|
Some(line) => {
|
||||||
|
return Err(anyhow!("unexpected worker handshake from {peer}: {line}"));
|
||||||
|
}
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !worker_name.is_empty() {
|
||||||
|
println!("worker connected from {peer}: {worker_name}");
|
||||||
|
} else {
|
||||||
|
println!("worker connected from {peer}");
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if interrupted.load(Ordering::SeqCst) {
|
||||||
|
let _ = write_half.write_all(b"SHUTDOWN\n").await;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(range) = next_range(&queue) else {
|
||||||
|
let _ = write_half.write_all(b"DONE\n").await;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = process_range(
|
||||||
|
&mut write_half,
|
||||||
|
&mut lines,
|
||||||
|
range,
|
||||||
|
&targets,
|
||||||
|
&database,
|
||||||
|
&ui,
|
||||||
|
tested_before_run,
|
||||||
|
&tested_this_run,
|
||||||
|
&open_this_run,
|
||||||
|
&config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
requeue_range(&queue, range);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_range(
|
||||||
|
write_half: &mut tokio::net::tcp::OwnedWriteHalf,
|
||||||
|
lines: &mut tokio::io::Lines<BufReader<tokio::net::tcp::OwnedReadHalf>>,
|
||||||
|
range: PendingRange,
|
||||||
|
targets: &Arc<Vec<Ipv4Addr>>,
|
||||||
|
database: &Arc<Database>,
|
||||||
|
ui: &Arc<TerminalUi>,
|
||||||
|
tested_before_run: usize,
|
||||||
|
tested_this_run: &Arc<AtomicUsize>,
|
||||||
|
open_this_run: &Arc<AtomicUsize>,
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<()> {
|
||||||
|
let ips = targets[range.start..range.end].to_vec();
|
||||||
|
let job_line = format!(
|
||||||
|
"JOB {} {} {} {} {}\n",
|
||||||
|
range.start,
|
||||||
|
range.end,
|
||||||
|
config.port,
|
||||||
|
config.timeout_ms,
|
||||||
|
format_ips(&ips)
|
||||||
|
);
|
||||||
|
write_half.write_all(job_line.as_bytes()).await?;
|
||||||
|
|
||||||
|
let Some(result_line) = lines.next_line().await? else {
|
||||||
|
return Err(anyhow!("worker disconnected before returning job"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = parse_result_line(&result_line)?;
|
||||||
|
if result.chunk_start != range.start || result.chunk_end != range.end {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"worker returned mismatched range: expected {}..{}, got {}..{}",
|
||||||
|
range.start,
|
||||||
|
range.end,
|
||||||
|
result.chunk_start,
|
||||||
|
result.chunk_end
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for ip in result.open_ips {
|
||||||
|
database.upsert_open_endpoint(ip, config.port).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let previous = database.load_chunk_record(range.chunk_start).await?;
|
||||||
|
let tested_before = previous
|
||||||
|
.as_ref()
|
||||||
|
.map(|record| record.tested_count)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let open_before = previous
|
||||||
|
.as_ref()
|
||||||
|
.map(|record| record.open_count)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let tested_count = tested_before + result.tested;
|
||||||
|
let open_count = open_before + result.open_count;
|
||||||
|
database
|
||||||
|
.save_chunk_record(&ChunkRecord {
|
||||||
|
chunk_start: range.chunk_start,
|
||||||
|
chunk_end: range.chunk_end,
|
||||||
|
tested_count,
|
||||||
|
open_count,
|
||||||
|
is_complete: result.tested >= range.len(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let tested_now = tested_this_run.fetch_add(result.tested, Ordering::SeqCst) + result.tested;
|
||||||
|
open_this_run.fetch_add(result.open_count, Ordering::SeqCst);
|
||||||
|
|
||||||
|
if ui.is_enabled() {
|
||||||
|
let current_ip = ips
|
||||||
|
.last()
|
||||||
|
.map(|ip| ip.to_string())
|
||||||
|
.unwrap_or_else(|| "concluído".to_string());
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip,
|
||||||
|
tested: tested_before_run + tested_now,
|
||||||
|
total: targets.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requeue_range(queue: &Arc<Mutex<VecDeque<PendingRange>>>, range: PendingRange) {
|
||||||
|
queue
|
||||||
|
.lock()
|
||||||
|
.expect("queue mutex poisoned")
|
||||||
|
.push_front(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_range(queue: &Arc<Mutex<VecDeque<PendingRange>>>) -> Option<PendingRange> {
|
||||||
|
queue.lock().expect("queue mutex poisoned").pop_front()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct JobResult {
|
||||||
|
chunk_start: usize,
|
||||||
|
chunk_end: usize,
|
||||||
|
tested: usize,
|
||||||
|
open_count: usize,
|
||||||
|
open_ips: Vec<Ipv4Addr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_result_line(line: &str) -> Result<JobResult> {
|
||||||
|
let mut parts = line.splitn(6, ' ');
|
||||||
|
let kind = parts.next().unwrap_or("");
|
||||||
|
if kind != "RESULT" {
|
||||||
|
return Err(anyhow!("unexpected worker message: {line}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_start = parse_usize_part(parts.next(), "chunk_start", line)?;
|
||||||
|
let chunk_end = parse_usize_part(parts.next(), "chunk_end", line)?;
|
||||||
|
let tested = parse_usize_part(parts.next(), "tested", line)?;
|
||||||
|
let open_count = parse_usize_part(parts.next(), "open_count", line)?;
|
||||||
|
let ips_part = parts.next().unwrap_or("");
|
||||||
|
let open_ips = if ips_part.trim().is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
ips_part
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|entry| !entry.is_empty())
|
||||||
|
.map(|entry| {
|
||||||
|
entry
|
||||||
|
.parse::<Ipv4Addr>()
|
||||||
|
.with_context(|| format!("invalid IP in worker result: {entry}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
if open_ips.len() != open_count {
|
||||||
|
return Err(anyhow!(
|
||||||
|
"open_count mismatch in worker result: declared {}, received {}",
|
||||||
|
open_count,
|
||||||
|
open_ips.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(JobResult {
|
||||||
|
chunk_start,
|
||||||
|
chunk_end,
|
||||||
|
tested,
|
||||||
|
open_count,
|
||||||
|
open_ips,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_usize_part(part: Option<&str>, name: &str, line: &str) -> Result<usize> {
|
||||||
|
let value = part.ok_or_else(|| anyhow!("missing {name} in worker message: {line}"))?;
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| anyhow!("invalid {name} in worker message: {line}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_ips(ips: &[Ipv4Addr]) -> String {
|
||||||
|
ips.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
use crate::models::ChunkRecord;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use tokio_postgres::{Client, NoTls};
|
||||||
|
|
||||||
|
pub struct Database {
|
||||||
|
client: Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Database {
|
||||||
|
pub async fn open(database_url: impl AsRef<str>) -> Result<Self> {
|
||||||
|
let database_url = database_url.as_ref().trim();
|
||||||
|
let (client, connection) = tokio_postgres::connect(database_url, NoTls)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to connect to postgres at {database_url}"))?;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = connection.await {
|
||||||
|
eprintln!("postgres connection error: {err}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let db = Self { client };
|
||||||
|
db.init().await?;
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn init(&self) -> Result<()> {
|
||||||
|
self.client
|
||||||
|
.batch_execute(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS open_rtsp_endpoint (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
ip INET NOT NULL UNIQUE,
|
||||||
|
port INTEGER NOT NULL,
|
||||||
|
scanned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS open_rtsp_endpoint_port_idx
|
||||||
|
ON open_rtsp_endpoint (port);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS scan_chunks (
|
||||||
|
chunk_start BIGINT PRIMARY KEY NOT NULL,
|
||||||
|
chunk_end BIGINT NOT NULL,
|
||||||
|
tested_count BIGINT NOT NULL DEFAULT 0,
|
||||||
|
open_count BIGINT NOT NULL DEFAULT 0,
|
||||||
|
is_complete BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("failed to initialize postgres schema")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_chunk_records(&self) -> Result<Vec<ChunkRecord>> {
|
||||||
|
let rows = self
|
||||||
|
.client
|
||||||
|
.query(
|
||||||
|
r#"
|
||||||
|
SELECT chunk_start, chunk_end, tested_count, open_count, is_complete
|
||||||
|
FROM scan_chunks
|
||||||
|
ORDER BY chunk_start ASC
|
||||||
|
"#,
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("failed to load scan chunks")?;
|
||||||
|
|
||||||
|
rows.into_iter().map(row_to_chunk_record).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn load_chunk_record(&self, chunk_start: usize) -> Result<Option<ChunkRecord>> {
|
||||||
|
let chunk_start = usize_to_i64(chunk_start, "chunk_start")?;
|
||||||
|
let row = self
|
||||||
|
.client
|
||||||
|
.query_opt(
|
||||||
|
r#"
|
||||||
|
SELECT chunk_start, chunk_end, tested_count, open_count, is_complete
|
||||||
|
FROM scan_chunks
|
||||||
|
WHERE chunk_start = $1
|
||||||
|
"#,
|
||||||
|
&[&chunk_start],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("failed to load scan chunk")?;
|
||||||
|
|
||||||
|
row.map(row_to_chunk_record).transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn save_chunk_record(&self, record: &ChunkRecord) -> Result<()> {
|
||||||
|
let chunk_start = usize_to_i64(record.chunk_start, "chunk_start")?;
|
||||||
|
let chunk_end = usize_to_i64(record.chunk_end, "chunk_end")?;
|
||||||
|
let tested_count = usize_to_i64(record.tested_count, "tested_count")?;
|
||||||
|
let open_count = usize_to_i64(record.open_count, "open_count")?;
|
||||||
|
|
||||||
|
self.client
|
||||||
|
.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO scan_chunks (
|
||||||
|
chunk_start,
|
||||||
|
chunk_end,
|
||||||
|
tested_count,
|
||||||
|
open_count,
|
||||||
|
is_complete,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
|
||||||
|
ON CONFLICT (chunk_start) DO UPDATE SET
|
||||||
|
chunk_end = EXCLUDED.chunk_end,
|
||||||
|
tested_count = EXCLUDED.tested_count,
|
||||||
|
open_count = EXCLUDED.open_count,
|
||||||
|
is_complete = EXCLUDED.is_complete,
|
||||||
|
updated_at = NOW()
|
||||||
|
"#,
|
||||||
|
&[
|
||||||
|
&chunk_start,
|
||||||
|
&chunk_end,
|
||||||
|
&tested_count,
|
||||||
|
&open_count,
|
||||||
|
&record.is_complete,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("failed to save scan chunk")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn upsert_open_endpoint(&self, ip: std::net::Ipv4Addr, port: u16) -> Result<()> {
|
||||||
|
let ip = ip.to_string();
|
||||||
|
let port = i32::from(port);
|
||||||
|
|
||||||
|
self.client
|
||||||
|
.execute(
|
||||||
|
r#"
|
||||||
|
INSERT INTO open_rtsp_endpoint (
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
scanned_at,
|
||||||
|
last_seen_at,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
VALUES ($1::inet, $2, NOW(), NOW(), NOW(), NOW())
|
||||||
|
ON CONFLICT (ip) DO UPDATE SET
|
||||||
|
port = EXCLUDED.port,
|
||||||
|
scanned_at = NOW(),
|
||||||
|
last_seen_at = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
"#,
|
||||||
|
&[&ip, &port],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("failed to upsert open endpoint")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn count_open_endpoints(&self, port: u16) -> Result<usize> {
|
||||||
|
let port = i32::from(port);
|
||||||
|
let count: i64 = self
|
||||||
|
.client
|
||||||
|
.query_one(
|
||||||
|
"SELECT COUNT(*)::BIGINT FROM open_rtsp_endpoint WHERE port = $1",
|
||||||
|
&[&port],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("failed to count open endpoints")?
|
||||||
|
.get(0);
|
||||||
|
|
||||||
|
i64_to_usize(count, "open endpoint count")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn summarize_chunks(&self) -> Result<(usize, usize)> {
|
||||||
|
let row = self
|
||||||
|
.client
|
||||||
|
.query_one(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
COALESCE(SUM(tested_count), 0)::BIGINT,
|
||||||
|
COALESCE(SUM(open_count), 0)::BIGINT
|
||||||
|
FROM scan_chunks
|
||||||
|
"#,
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.context("failed to summarize scan chunks")?;
|
||||||
|
|
||||||
|
let tested = i64_to_usize(row.get::<_, i64>(0), "tested count")?;
|
||||||
|
let open = i64_to_usize(row.get::<_, i64>(1), "open count")?;
|
||||||
|
Ok((tested, open))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row_to_chunk_record(row: tokio_postgres::Row) -> Result<ChunkRecord> {
|
||||||
|
Ok(ChunkRecord {
|
||||||
|
chunk_start: i64_to_usize(row.get::<_, i64>(0), "chunk_start")?,
|
||||||
|
chunk_end: i64_to_usize(row.get::<_, i64>(1), "chunk_end")?,
|
||||||
|
tested_count: i64_to_usize(row.get::<_, i64>(2), "tested_count")?,
|
||||||
|
open_count: i64_to_usize(row.get::<_, i64>(3), "open_count")?,
|
||||||
|
is_complete: row.get::<_, bool>(4),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn i64_to_usize(value: i64, field: &str) -> Result<usize> {
|
||||||
|
usize::try_from(value).with_context(|| format!("{field} out of range: {value}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usize_to_i64(value: usize, field: &str) -> Result<i64> {
|
||||||
|
i64::try_from(value).with_context(|| format!("{field} out of range: {value}"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::net::Ipv4Addr;
|
||||||
|
|
||||||
|
pub fn cidr_to_hosts(cidr: &str) -> Result<Vec<Ipv4Addr>> {
|
||||||
|
let (network, prefix) = cidr
|
||||||
|
.split_once('/')
|
||||||
|
.ok_or_else(|| anyhow!("CIDR inválido: {cidr}"))?;
|
||||||
|
|
||||||
|
let prefix: u8 = prefix
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("máscara de sub-rede inválida: {prefix}"))?;
|
||||||
|
if prefix > 32 {
|
||||||
|
return Err(anyhow!("máscara de sub-rede inválida: {prefix}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let network: Ipv4Addr = network
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("endereço IP inválido: {network}"))?;
|
||||||
|
|
||||||
|
let network_u32 = u32::from(network);
|
||||||
|
let mask = if prefix == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
u32::MAX << (32 - prefix)
|
||||||
|
};
|
||||||
|
let network_base = network_u32 & mask;
|
||||||
|
let broadcast = if prefix == 32 {
|
||||||
|
network_base
|
||||||
|
} else {
|
||||||
|
network_base | !mask
|
||||||
|
};
|
||||||
|
|
||||||
|
let start = if prefix >= 31 {
|
||||||
|
network_base
|
||||||
|
} else {
|
||||||
|
network_base + 1
|
||||||
|
};
|
||||||
|
let end = if prefix >= 31 {
|
||||||
|
broadcast
|
||||||
|
} else {
|
||||||
|
broadcast - 1
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut hosts = Vec::new();
|
||||||
|
for ip_u32 in start..=end {
|
||||||
|
hosts.push(Ipv4Addr::from(ip_u32));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(hosts)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn is_routable_ipv4(ip: Ipv4Addr) -> bool {
|
||||||
|
let [a, b, c, _d] = ip.octets();
|
||||||
|
|
||||||
|
if a == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 10 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 100 && (64..=127).contains(&b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 127 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 169 && b == 254 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 172 && (16..=31).contains(&b) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 192 && b == 0 && c == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 192 && b == 0 && c == 2 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 192 && b == 88 && c == 99 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 192 && b == 168 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 198 && (b == 18 || b == 19) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 198 && b == 51 && c == 100 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a == 203 && b == 0 && c == 113 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (224..=239).contains(&a) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if a >= 240 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
+124
@@ -0,0 +1,124 @@
|
|||||||
|
mod config;
|
||||||
|
mod coordinator;
|
||||||
|
mod db;
|
||||||
|
mod ip;
|
||||||
|
mod models;
|
||||||
|
mod scan;
|
||||||
|
mod terminal;
|
||||||
|
mod worker;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use config::{Config, Mode};
|
||||||
|
use db::Database;
|
||||||
|
use std::sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
use terminal::{stdout_is_terminal, TerminalUi};
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
|
let config = Config::from_env_and_args()?;
|
||||||
|
if matches!(config.mode, Mode::Local | Mode::Coordinator) {
|
||||||
|
assert_local_cidrs(&config.cidrs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let database = Arc::new(Database::open(&config.database_url).await?);
|
||||||
|
let ui = Arc::new(TerminalUi::new(stdout_is_terminal()));
|
||||||
|
let interrupted = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
|
{
|
||||||
|
let interrupted = Arc::clone(&interrupted);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = tokio::signal::ctrl_c().await;
|
||||||
|
interrupted.store(true, Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match config.mode {
|
||||||
|
Mode::Local => scan::run_local_scan(config, database, ui, interrupted).await,
|
||||||
|
Mode::Coordinator => coordinator::run_coordinator(config, database, ui, interrupted).await,
|
||||||
|
Mode::Worker => worker::run_worker(config, ui).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = result {
|
||||||
|
eprintln!("error: {err:#}");
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_local_cidrs(cidrs: &[String]) -> Result<()> {
|
||||||
|
if cidrs.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"nenhum CIDR foi informado. Use SCAN_CIDRS, SCAN_CIDR_FILE ou --cidrs/--cidr-file"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for cidr in cidrs {
|
||||||
|
assert_local_cidr_inner(cidr)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_local_cidr_inner(cidr: &str) -> Result<()> {
|
||||||
|
let (network, prefix) = cidr
|
||||||
|
.split_once('/')
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("CIDR inválido: {cidr}"))?;
|
||||||
|
|
||||||
|
let prefix: u8 = prefix
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| anyhow::anyhow!("máscara de sub-rede inválida: {prefix}"))?;
|
||||||
|
if prefix > 32 {
|
||||||
|
return Err(anyhow::anyhow!("máscara de sub-rede inválida: {prefix}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let ip: std::net::Ipv4Addr = network
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| anyhow::anyhow!("endereço IP inválido: {network}"))?;
|
||||||
|
|
||||||
|
let start = u32::from(ip) & cidr_mask(prefix);
|
||||||
|
let end = if prefix == 32 {
|
||||||
|
start
|
||||||
|
} else {
|
||||||
|
start | !cidr_mask(prefix)
|
||||||
|
};
|
||||||
|
|
||||||
|
let private_ranges = [
|
||||||
|
(
|
||||||
|
u32::from(std::net::Ipv4Addr::new(10, 0, 0, 0)),
|
||||||
|
u32::from(std::net::Ipv4Addr::new(10, 255, 255, 255)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
u32::from(std::net::Ipv4Addr::new(172, 16, 0, 0)),
|
||||||
|
u32::from(std::net::Ipv4Addr::new(172, 31, 255, 255)),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
u32::from(std::net::Ipv4Addr::new(192, 168, 0, 0)),
|
||||||
|
u32::from(std::net::Ipv4Addr::new(192, 168, 255, 255)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let allowed = private_ranges
|
||||||
|
.iter()
|
||||||
|
.any(|(range_start, range_end)| start >= *range_start && end <= *range_end);
|
||||||
|
if !allowed {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"CIDR fora de redes locais permitidas: {cidr}. Use 10.0.0.0/8, 172.16.0.0/12 ou 192.168.0.0/16"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cidr_mask(prefix: u8) -> u32 {
|
||||||
|
if prefix == 0 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
u32::MAX << (32 - prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct PendingRange {
|
||||||
|
pub chunk_start: usize,
|
||||||
|
pub chunk_end: usize,
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PendingRange {
|
||||||
|
pub fn len(self) -> usize {
|
||||||
|
self.end.saturating_sub(self.start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChunkRecord {
|
||||||
|
pub chunk_start: usize,
|
||||||
|
pub chunk_end: usize,
|
||||||
|
pub tested_count: usize,
|
||||||
|
pub open_count: usize,
|
||||||
|
pub is_complete: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ScanTotals {
|
||||||
|
pub tested: usize,
|
||||||
|
pub open: usize,
|
||||||
|
}
|
||||||
+305
@@ -0,0 +1,305 @@
|
|||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
db::Database,
|
||||||
|
ip::cidr_to_hosts,
|
||||||
|
models::{ChunkRecord, PendingRange, ScanTotals},
|
||||||
|
terminal::{ScreenState, TerminalUi},
|
||||||
|
};
|
||||||
|
use anyhow::Result;
|
||||||
|
use std::{
|
||||||
|
net::Ipv4Addr,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
use tokio::{net::TcpStream, time::timeout};
|
||||||
|
|
||||||
|
pub async fn run_local_scan(
|
||||||
|
config: Config,
|
||||||
|
database: Arc<Database>,
|
||||||
|
ui: Arc<TerminalUi>,
|
||||||
|
interrupted: Arc<AtomicBool>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let targets = load_targets(&config.cidrs)?;
|
||||||
|
let target_count = targets.len();
|
||||||
|
let chunk_records = database.load_chunk_records().await?;
|
||||||
|
let (tested_before_run, _open_before_run) = database.summarize_chunks().await?;
|
||||||
|
let pending_ranges = build_pending_ranges(target_count, config.chunk_size, &chunk_records);
|
||||||
|
let pending_count: usize = pending_ranges.iter().map(|range| range.len()).sum();
|
||||||
|
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.print_banner();
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: if pending_count > 0 {
|
||||||
|
targets[pending_ranges[0].start].to_string()
|
||||||
|
} else {
|
||||||
|
"concluído".to_string()
|
||||||
|
},
|
||||||
|
tested: tested_before_run,
|
||||||
|
total: target_count,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"Scanning {} hosts on {} CIDRs, port {}, concurrency {}, chunk size {}. Pending {}, already tested {}.",
|
||||||
|
target_count,
|
||||||
|
config.cidrs.len(),
|
||||||
|
config.port,
|
||||||
|
config.concurrency,
|
||||||
|
config.chunk_size,
|
||||||
|
pending_count,
|
||||||
|
tested_before_run
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending_ranges.is_empty() {
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: "concluído".to_string(),
|
||||||
|
tested: target_count,
|
||||||
|
total: target_count,
|
||||||
|
});
|
||||||
|
ui.finish();
|
||||||
|
} else {
|
||||||
|
println!("Scan concluído em 0.00s");
|
||||||
|
println!("Total de hosts testados: 0");
|
||||||
|
println!("Portas abertas encontradas neste scan: 0");
|
||||||
|
println!(
|
||||||
|
"Total de portas abertas no banco: {}",
|
||||||
|
database.count_open_endpoints(config.port).await?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let next_range = Arc::new(AtomicUsize::new(0));
|
||||||
|
let tested_this_run = Arc::new(AtomicUsize::new(0));
|
||||||
|
let open_this_run = Arc::new(AtomicUsize::new(0));
|
||||||
|
let mut handles = Vec::new();
|
||||||
|
for _ in 0..config.concurrency.max(1) {
|
||||||
|
let ranges = pending_ranges.clone();
|
||||||
|
let next_range = Arc::clone(&next_range);
|
||||||
|
let tested_this_run = Arc::clone(&tested_this_run);
|
||||||
|
let open_this_run = Arc::clone(&open_this_run);
|
||||||
|
let database = Arc::clone(&database);
|
||||||
|
let ui = Arc::clone(&ui);
|
||||||
|
let targets = targets.clone();
|
||||||
|
let interrupted = Arc::clone(&interrupted);
|
||||||
|
let config = config.clone();
|
||||||
|
|
||||||
|
handles.push(tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
if interrupted.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = next_range.fetch_add(1, Ordering::SeqCst);
|
||||||
|
let Some(range) = ranges.get(index).copied() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
let chunk_stats = scan_range(
|
||||||
|
range,
|
||||||
|
&targets,
|
||||||
|
&config,
|
||||||
|
&database,
|
||||||
|
&ui,
|
||||||
|
&tested_this_run,
|
||||||
|
&open_this_run,
|
||||||
|
&interrupted,
|
||||||
|
tested_before_run,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
persist_range(&database, range, chunk_stats).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok::<(), anyhow::Error>(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for handle in handles {
|
||||||
|
handle.await??;
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = start.elapsed().as_secs_f64();
|
||||||
|
let total_open_count = database.count_open_endpoints(config.port).await?;
|
||||||
|
|
||||||
|
if interrupted.load(Ordering::SeqCst) {
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: "interrompido".to_string(),
|
||||||
|
tested: tested_before_run + tested_this_run.load(Ordering::SeqCst),
|
||||||
|
total: target_count,
|
||||||
|
});
|
||||||
|
ui.finish();
|
||||||
|
} else {
|
||||||
|
println!("Interrompido após {:.2}s. Progresso salvo.", duration);
|
||||||
|
println!(
|
||||||
|
"Hosts testados nesta execução: {}",
|
||||||
|
tested_this_run.load(Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Portas abertas encontradas: {}",
|
||||||
|
open_this_run.load(Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: "concluído".to_string(),
|
||||||
|
tested: target_count,
|
||||||
|
total: target_count,
|
||||||
|
});
|
||||||
|
ui.finish();
|
||||||
|
} else {
|
||||||
|
println!("Scan concluído em {:.2}s", duration);
|
||||||
|
println!(
|
||||||
|
"Total de hosts testados: {}",
|
||||||
|
tested_this_run.load(Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Portas abertas encontradas neste scan: {}",
|
||||||
|
open_this_run.load(Ordering::SeqCst)
|
||||||
|
);
|
||||||
|
println!("Total de portas abertas no banco: {}", total_open_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn scan_range(
|
||||||
|
range: PendingRange,
|
||||||
|
targets: &[Ipv4Addr],
|
||||||
|
config: &Config,
|
||||||
|
database: &Database,
|
||||||
|
ui: &TerminalUi,
|
||||||
|
tested_this_run: &AtomicUsize,
|
||||||
|
open_this_run: &AtomicUsize,
|
||||||
|
interrupted: &AtomicBool,
|
||||||
|
tested_before_run: usize,
|
||||||
|
) -> Result<ScanTotals> {
|
||||||
|
let mut totals = ScanTotals::default();
|
||||||
|
|
||||||
|
for index in range.start..range.end {
|
||||||
|
if interrupted.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ip = targets[index];
|
||||||
|
let is_open = check_port_open(ip, config.port, config.timeout_ms).await;
|
||||||
|
totals.tested += 1;
|
||||||
|
let tested_now = tested_this_run.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
|
||||||
|
if is_open {
|
||||||
|
totals.open += 1;
|
||||||
|
open_this_run.fetch_add(1, Ordering::SeqCst);
|
||||||
|
database.upsert_open_endpoint(ip, config.port).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if should_render_progress(tested_now, range.end - range.start) || index + 1 == range.end {
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: ip.to_string(),
|
||||||
|
tested: tested_before_run + tested_now,
|
||||||
|
total: targets.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(totals)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn persist_range(database: &Database, range: PendingRange, totals: ScanTotals) -> Result<()> {
|
||||||
|
let previous = database.load_chunk_record(range.chunk_start).await?;
|
||||||
|
let tested_before = previous
|
||||||
|
.as_ref()
|
||||||
|
.map(|record| record.tested_count)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let open_before = previous
|
||||||
|
.as_ref()
|
||||||
|
.map(|record| record.open_count)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let tested_count = tested_before + totals.tested;
|
||||||
|
let open_count = open_before + totals.open;
|
||||||
|
let record = ChunkRecord {
|
||||||
|
chunk_start: range.chunk_start,
|
||||||
|
chunk_end: range.chunk_end,
|
||||||
|
tested_count,
|
||||||
|
open_count,
|
||||||
|
is_complete: tested_count >= range.len() + tested_before,
|
||||||
|
};
|
||||||
|
|
||||||
|
database.save_chunk_record(&record).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_pending_ranges(
|
||||||
|
total_targets: usize,
|
||||||
|
chunk_size: usize,
|
||||||
|
records: &[ChunkRecord],
|
||||||
|
) -> Vec<PendingRange> {
|
||||||
|
let mut ranges = Vec::new();
|
||||||
|
let chunk_size = chunk_size.max(1);
|
||||||
|
|
||||||
|
for chunk_start in (0..total_targets).step_by(chunk_size) {
|
||||||
|
let chunk_end = (chunk_start + chunk_size).min(total_targets);
|
||||||
|
let chunk_len = chunk_end.saturating_sub(chunk_start);
|
||||||
|
let record = records
|
||||||
|
.iter()
|
||||||
|
.find(|record| record.chunk_start == chunk_start);
|
||||||
|
let tested_count = record
|
||||||
|
.map(|record| {
|
||||||
|
if record.is_complete {
|
||||||
|
chunk_len
|
||||||
|
} else {
|
||||||
|
record.tested_count.min(chunk_len)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
if tested_count < chunk_len {
|
||||||
|
ranges.push(PendingRange {
|
||||||
|
chunk_start,
|
||||||
|
chunk_end,
|
||||||
|
start: chunk_start + tested_count,
|
||||||
|
end: chunk_end,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ranges
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_port_open(ip: Ipv4Addr, port: u16, timeout_ms: u64) -> bool {
|
||||||
|
let address = std::net::SocketAddr::from((ip, port));
|
||||||
|
matches!(
|
||||||
|
timeout(
|
||||||
|
std::time::Duration::from_millis(timeout_ms),
|
||||||
|
TcpStream::connect(address)
|
||||||
|
)
|
||||||
|
.await,
|
||||||
|
Ok(Ok(_))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_targets(cidrs: &[String]) -> Result<Vec<Ipv4Addr>> {
|
||||||
|
let mut targets = Vec::new();
|
||||||
|
|
||||||
|
for cidr in cidrs {
|
||||||
|
targets.extend(cidr_to_hosts(cidr)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(targets)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_render_progress(test_index: usize, chunk_len: usize) -> bool {
|
||||||
|
if chunk_len <= 8 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
test_index % 4 == 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::io::{self, IsTerminal, Write};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
pub const BANNER: [&str; 9] = [
|
||||||
|
" ░██████ ░██████ ",
|
||||||
|
" ░██ ░██ ░██ ░██ ",
|
||||||
|
"░██ ░██████ ░█████████████░██ ░███████ ░██████ ░████████ ░████████ ░███████ ░██░████ ",
|
||||||
|
"░██ ░██░██ ░██ ░██░████████░██ ░██ ░██ ░██ ░██░██ ░██░██ ░██░███ ",
|
||||||
|
"░██ ░███████░██ ░██ ░██ ░██░██ ░███████ ░██ ░██░██ ░██░█████████░██ ",
|
||||||
|
" ░██ ░██░██ ░██░██ ░██ ░██░██ ░██░██ ░██░██ ░██░██ ░██░██ ░██░██ ░██ ",
|
||||||
|
" ░██████ ░█████░█░██ ░██ ░██ ░██████ ░███████ ░█████░██░██ ░██░██ ░██ ░███████ ░██ ",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ScreenState {
|
||||||
|
pub current_ip: String,
|
||||||
|
pub tested: usize,
|
||||||
|
pub total: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TerminalUi {
|
||||||
|
enabled: bool,
|
||||||
|
lock: Mutex<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalUi {
|
||||||
|
pub fn new(enabled: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
enabled,
|
||||||
|
lock: Mutex::new(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_enabled(&self) -> bool {
|
||||||
|
self.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_banner(&self) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _guard = self.lock.lock().expect("terminal mutex poisoned");
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
let _ = write!(stdout, "\x1b[2J\x1b[H\x1b[?25l");
|
||||||
|
for line in BANNER {
|
||||||
|
let _ = writeln!(stdout, "{line}");
|
||||||
|
}
|
||||||
|
let _ = writeln!(stdout, "IP: aguardando...");
|
||||||
|
let _ = writeln!(
|
||||||
|
stdout,
|
||||||
|
"Progress: [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] | 0.0%"
|
||||||
|
);
|
||||||
|
let _ = stdout.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, state: &ScreenState) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _guard = self.lock.lock().expect("terminal mutex poisoned");
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
let percent = if state.total == 0 {
|
||||||
|
100.0
|
||||||
|
} else {
|
||||||
|
((state.tested as f64 / state.total as f64) * 100.0).min(100.0)
|
||||||
|
};
|
||||||
|
let bar_width = 36usize;
|
||||||
|
let filled = ((percent / 100.0) * bar_width as f64).round() as usize;
|
||||||
|
let empty = bar_width.saturating_sub(filled);
|
||||||
|
let bar = format!("{}{}", "█".repeat(filled), "░".repeat(empty));
|
||||||
|
let ip = format!("{:<15}", state.current_ip);
|
||||||
|
let percent = format!("{:>5.1}%", percent);
|
||||||
|
|
||||||
|
let _ = write!(
|
||||||
|
stdout,
|
||||||
|
"\x1b[2F\x1b[2KIP: {ip}\n\x1b[2KProgress: [{bar}] | {percent}\n"
|
||||||
|
);
|
||||||
|
let _ = stdout.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(&self) {
|
||||||
|
if !self.enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _guard = self.lock.lock().expect("terminal mutex poisoned");
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
let _ = write!(stdout, "\x1b[?25h");
|
||||||
|
let _ = stdout.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stdout_is_terminal() -> bool {
|
||||||
|
io::stdout().is_terminal()
|
||||||
|
}
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
scan::check_port_open,
|
||||||
|
terminal::{ScreenState, TerminalUi},
|
||||||
|
};
|
||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use std::{net::Ipv4Addr, sync::Arc};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||||
|
net::TcpStream,
|
||||||
|
task::JoinSet,
|
||||||
|
time::sleep,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn run_worker(config: Config, ui: Arc<TerminalUi>) -> Result<()> {
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.print_banner();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = connect_with_retry(config.coordinator_addr).await?;
|
||||||
|
let (read_half, mut write_half) = stream.into_split();
|
||||||
|
let mut lines = BufReader::new(read_half).lines();
|
||||||
|
write_half
|
||||||
|
.write_all(format!("HELLO {}\n", config.worker_name).as_bytes())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let Some(line) = lines.next_line().await? else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
if line == "DONE" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line == "SHUTDOWN" {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let job = parse_job_line(&line)?;
|
||||||
|
let result = execute_job(
|
||||||
|
&job.ips,
|
||||||
|
config.port,
|
||||||
|
config.timeout_ms,
|
||||||
|
config.concurrency,
|
||||||
|
&ui,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result_line = format!(
|
||||||
|
"RESULT {} {} {} {} {}\n",
|
||||||
|
job.chunk_start,
|
||||||
|
job.chunk_end,
|
||||||
|
result.tested,
|
||||||
|
result.open_ips.len(),
|
||||||
|
format_ips(&result.open_ips)
|
||||||
|
);
|
||||||
|
write_half.write_all(result_line.as_bytes()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: "concluído".to_string(),
|
||||||
|
tested: 0,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
ui.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_with_retry(addr: std::net::SocketAddr) -> Result<TcpStream> {
|
||||||
|
let mut attempts = 0usize;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match TcpStream::connect(addr).await {
|
||||||
|
Ok(stream) => return Ok(stream),
|
||||||
|
Err(err) => {
|
||||||
|
attempts += 1;
|
||||||
|
if attempts >= 30 {
|
||||||
|
return Err(err).with_context(|| format!("failed to connect to {addr}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Job {
|
||||||
|
chunk_start: usize,
|
||||||
|
chunk_end: usize,
|
||||||
|
ips: Vec<Ipv4Addr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct JobOutcome {
|
||||||
|
tested: usize,
|
||||||
|
open_ips: Vec<Ipv4Addr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_job_line(line: &str) -> Result<Job> {
|
||||||
|
let mut parts = line.splitn(6, ' ');
|
||||||
|
let kind = parts.next().unwrap_or("");
|
||||||
|
if kind != "JOB" {
|
||||||
|
return Err(anyhow!("unexpected coordinator message: {line}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk_start = parse_usize_part(parts.next(), "chunk_start", line)?;
|
||||||
|
let chunk_end = parse_usize_part(parts.next(), "chunk_end", line)?;
|
||||||
|
let _port = parse_usize_part(parts.next(), "port", line)?;
|
||||||
|
let _timeout_ms = parse_usize_part(parts.next(), "timeout_ms", line)?;
|
||||||
|
let ips_part = parts.next().unwrap_or("");
|
||||||
|
let ips = if ips_part.trim().is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
ips_part
|
||||||
|
.split(',')
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|entry| !entry.is_empty())
|
||||||
|
.map(|entry| {
|
||||||
|
entry
|
||||||
|
.parse::<Ipv4Addr>()
|
||||||
|
.with_context(|| format!("invalid IP in job: {entry}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Job {
|
||||||
|
chunk_start,
|
||||||
|
chunk_end,
|
||||||
|
ips,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_job(
|
||||||
|
ips: &[Ipv4Addr],
|
||||||
|
port: u16,
|
||||||
|
timeout_ms: u64,
|
||||||
|
concurrency: usize,
|
||||||
|
ui: &TerminalUi,
|
||||||
|
) -> Result<JobOutcome> {
|
||||||
|
let mut tested = 0usize;
|
||||||
|
let mut open_ips = Vec::new();
|
||||||
|
let concurrency = concurrency.max(1);
|
||||||
|
let mut index = 0usize;
|
||||||
|
|
||||||
|
while index < ips.len() {
|
||||||
|
let end = (index + concurrency).min(ips.len());
|
||||||
|
let mut join_set = JoinSet::new();
|
||||||
|
|
||||||
|
for ip in ips[index..end].iter().copied() {
|
||||||
|
join_set.spawn(async move { (ip, check_port_open(ip, port, timeout_ms).await) });
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(join_result) = join_set.join_next().await {
|
||||||
|
let (ip, is_open) = join_result?;
|
||||||
|
tested += 1;
|
||||||
|
if is_open {
|
||||||
|
open_ips.push(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui.is_enabled() {
|
||||||
|
ui.render(&ScreenState {
|
||||||
|
current_ip: ip.to_string(),
|
||||||
|
tested,
|
||||||
|
total: ips.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(JobOutcome { tested, open_ips })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_usize_part(part: Option<&str>, name: &str, line: &str) -> Result<usize> {
|
||||||
|
let value = part.ok_or_else(|| anyhow!("missing {name} in message: {line}"))?;
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.parse()
|
||||||
|
.map_err(|_| anyhow!("invalid {name} in message: {line}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_ips(ips: &[Ipv4Addr]) -> String {
|
||||||
|
ips.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user