5 Commits

Author SHA1 Message Date
Ваше Имя 7efd5cbce5 feat(scanner): add subnet ping scan and TCP port scans for alive hosts
Introduce subnet scanning using CIDR prefix notation and perform TCP port scans
on each discovered alive host. The change replaces the full port scan with a
two-step process: first pinging the subnet to find active devices, then scanning
their open TCP ports.
2025-10-07 18:42:05 +04:00
Ваше Имя 0e8d77971d refactor(port_scanner): rework to use Boost.Asio for asynchronous port scanning
Rewritten to use Boost.Asio for asynchronous scanning, introducing timeout
management via timers and concurrent port probing up to 256. Added openPorts()
method to retrieve results.
2025-10-07 18:41:37 +04:00
Ваше Имя 025f49a5fd feat(core): add LocalPingScanner for subnet host discovery 2025-10-07 18:40:54 +04:00
Ваше Имя 902959f658 feat(dependencies): add Poco library
Introduce Poco library dependencies for port_scanner target, including Net and
Foundation components, alongside Boost.
2025-10-07 18:40:24 +04:00
Ваше Имя fcbf6c740c feat(portscan): add CIDR prefix support and base IP handling
Add CIDR prefix option to define IP range for scanning, with default base IP of
10.0.1.0 and prefix 24. Update methods to retrieve base IP and prefix
separately.
2025-10-07 18:38:20 +04:00
8 changed files with 320 additions and 166 deletions
+8 -6
View File
@@ -4,7 +4,6 @@
PortScanCli::PortScanCli() PortScanCli::PortScanCli()
: CliBase("Port Scanner Options") : CliBase("Port Scanner Options")
, ip_("")
{ {
} }
@@ -13,14 +12,17 @@ void PortScanCli::setupOptions()
// clang-format off // clang-format off
desc_.add_options() desc_.add_options()
("help,h", "Show help") ("help,h", "Show help")
("ip,H", withDefault(ip_, "<IP>"), "Local IPv4 to scan"); ("ip,H", withDefault(ip_, "<IPv4>"), "Base IPv4 inside your LAN (e.g. 10.0.1.42)")
("prefix,p", withDefault(prefix_, "<0..32>"), "CIDR prefix length (e.g. 24)");
// clang-format on // clang-format on
} }
std::string PortScanCli::getIp() const int PortScanCli::getPrefix() const
{
return vm_.at("prefix").as<int>();
}
std::string PortScanCli::getBaseIp() const
{ {
if (vm_["ip"].empty() == 0) {
return "127.0.0.1";
}
return vm_.at("ip").as<std::string>(); return vm_.at("ip").as<std::string>();
} }
+4 -2
View File
@@ -9,11 +9,13 @@ class PortScanCli : public CliBase, protected OptionUtils
public: public:
PortScanCli(); PortScanCli();
std::string getIp() const; int getPrefix() const;
std::string getBaseIp() const;
protected: protected:
void setupOptions() override; void setupOptions() override;
private: private:
std::string ip_; int prefix_{24};
std::string ip_{"10.0.1.0"};
}; };
+103
View File
@@ -0,0 +1,103 @@
#include "local_ping_scanner.hpp"
#include "logger.hpp"
#include <Poco/Net/ICMPClient.h>
#include <boost/asio/ip/address_v4.hpp>
#include <stdexcept>
LocalPingScanner::LocalPingScanner(
const std::string &baseIp, int prefixLen, int timeoutMs
)
: baseIp_(baseIp)
, prefixLen_(prefixLen)
, timeoutMs_(timeoutMs)
{
if (prefixLen_ < 0 || prefixLen_ > 32) {
Logger::error("Invalid prefix length in LocalPingScanner constructor");
throw std::runtime_error("Bad prefix length");
}
if (timeoutMs_ <= 0) {
timeoutMs_ = 2000;
}
}
const std::vector<std::string> &LocalPingScanner::alive() const
{
return alive_;
}
uint32_t LocalPingScanner::ipToU32(const std::string &ip)
{
using namespace boost::asio::ip;
return static_cast<uint32_t>(make_address_v4(ip).to_uint());
}
std::string LocalPingScanner::u32ToIp(uint32_t v)
{
using namespace boost::asio::ip;
return address_v4(v).to_string();
}
bool LocalPingScanner::pingHost(const std::string &ip) const
{
try {
return Poco::Net::ICMPClient::pingIPv4(
ip,
/*repeat=*/1,
/*dataSize=*/48,
/*ttl=*/64,
/*timeout=*/timeoutMs_
)
> 0;
} catch (const std::exception &ex) {
Logger::error(
std::string("Ping failed for host ") + ip + ": " + ex.what()
);
return false;
} catch (...) {
Logger::fatal(std::string("Unknown error pinging host ") + ip);
return false;
}
}
void LocalPingScanner::runImpl()
{
alive_.clear();
const uint32_t base = ipToU32(baseIp_);
const uint32_t mask =
(prefixLen_ == 0) ? 0u : (0xFFFFFFFFu << (32 - prefixLen_));
const uint32_t network = base & mask;
const uint32_t broadcast = network | (~mask);
// No hosts when prefix is too tight (e.g., /32).
if (broadcast - network <= 2) {
return;
}
const std::string selfIp = u32ToIp(base);
for (uint32_t v = network + 1; v < broadcast; ++v) {
const std::string ipStr = u32ToIp(v);
if (ipStr == selfIp) {
continue;
}
if (pingHost(ipStr)) {
alive_.push_back(ipStr);
}
}
}
void LocalPingScanner::run()
{
try {
runImpl();
} catch (const std::exception &ex) {
Logger::error(
std::string("Exception in LocalPingScanner::run: ") + ex.what()
);
} catch (...) {
Logger::fatal("Unknown exception in LocalPingScanner::run");
}
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
class LocalPingScanner
{
public:
LocalPingScanner(
const std::string &baseIp, int prefixLen, int timeoutMs = 2000
);
void run(); // blocking scan over subnet
const std::vector<std::string> &alive() const;
private:
static std::string u32ToIp(uint32_t v);
static uint32_t ipToU32(const std::string &ip);
bool pingHost(const std::string &ip) const;
void runImpl();
std::string baseIp_;
int prefixLen_;
int timeoutMs_;
std::vector<std::string> alive_;
};
+86 -102
View File
@@ -1,142 +1,126 @@
#include "port_scanner.hpp" #include "port_scanner.hpp"
#include "logger.hpp" #include "logger.hpp"
#include <boost/asio.hpp> #include <algorithm>
#include <string>
PortScanner::PortScanner( PortScanner::PortScanner(std::string ip, int timeoutMs)
const std::string &ip, const std::vector<uint16_t> &ports : ip_(std::move(ip))
) , timeoutMs_(timeoutMs)
: ports_(ports)
{ {
ErrorCode ec; if (timeoutMs_ <= 0) {
addr_ = boost::asio::ip::make_address(ip, ec); timeoutMs_ = 250;
ip_ok_ = !ec;
if (!ip_ok_) {
Logger::error(std::string("PortScanner: invalid IP: ") + ip);
} else {
Logger::debug(std::string("PortScanner: using IP: ") + ip);
} }
} }
void PortScanner::setTimeoutMs(int ms) PortScanner::Probe::Probe(IoContext &io, uint16_t p)
: port(p)
, socket(io)
, timer(io)
{ {
if (ms > 0) { }
timeout_ms_ = ms;
Logger::debug( const std::vector<uint16_t> &PortScanner::openPorts() const
std::string("PortScanner: timeout set to ") {
+ std::to_string(timeout_ms_) + " ms" return openPorts_;
);
}
} }
void PortScanner::run() void PortScanner::run()
{ {
if (!ip_ok_) { openPorts_.clear();
Logger::error("PortScanner::run - aborting: ip not ok"); nextPort_ = kFirstPort;
outstanding_ = 0;
active_.clear();
while (outstanding_ < kMaxConcurrency && nextPort_ <= kLastPort) {
startNext();
}
io_.run();
}
void PortScanner::startNext()
{
if (nextPort_ > kLastPort) {
return; return;
} }
for (uint16_t p : ports_) {
if (probe(p)) { const uint16_t port = static_cast<uint16_t>(nextPort_);
Logger::info(std::to_string(p)); nextPort_ += 1;
}
} startProbe(port);
} }
bool PortScanner::probe(uint16_t port) void PortScanner::startProbe(uint16_t port)
{ {
TcpSocket s(io_); // Lifetime of Probe is managed in 'active_' until probe finishes.
SteadyTimer t(io_); auto prPtr = std::make_unique<Probe>(io_, port);
auto ep = endpoint(port); Probe *pr = prPtr.get();
active_.push_back(std::move(prPtr));
Logger::debug( outstanding_ += 1;
std::string("PortScanner::probe - probing port ") + std::to_string(port)
);
armTimer(t, s); {
armConnect(s, ep); using namespace boost::asio::ip;
runUntilDone();
cancelTimer(t);
drainLeftovers();
close(s);
if (ok_) { TcpEndpoint ep(make_address_v4(ip_), port);
Logger::debug(std::string("Port ") + std::to_string(port) + " is OPEN");
} else {
Logger::debug(
std::string("Port ") + std::to_string(port) + " is CLOSED/FILTERED"
);
}
return ok_; pr->timer.expires_after(std::chrono::milliseconds(timeoutMs_));
}
TcpEndpoint PortScanner::endpoint(uint16_t port) const auto onTimer = [pr](const ErrorCode &ec) {
{ // Cancel socket on timeout; completion will run onConnect with
return TcpEndpoint(addr_, port); // ec=operation_aborted.
}
void PortScanner::armTimer(SteadyTimer &t, TcpSocket &s)
{
t.expires_after(std::chrono::milliseconds(timeout_ms_));
auto onTimeout = [&s](const ErrorCode &ec) {
if (!ec) { if (!ec) {
Logger::debug("Timer expired -> cancelling socket"); pr->socket.cancel();
s.cancel();
} }
}; };
t.async_wait(onTimeout); auto onAsyncConnect = [this, pr](const ErrorCode &ec) {
} onConnect(pr, ec);
void PortScanner::armConnect(TcpSocket &s, const TcpEndpoint &ep)
{
ok_ = false;
done_ = false;
auto onConnect = [this](const ErrorCode &ec) {
if (!ec) {
ok_ = true;
Logger::debug("async_connect succeeded");
} else {
ok_ = false;
Logger::debug(std::string("async_connect failed: ") + ec.message());
}
done_ = true;
}; };
s.async_connect(ep, onConnect); pr->timer.async_wait(onTimer);
} pr->socket.async_connect(ep, onAsyncConnect);
void PortScanner::runUntilDone()
{
io_.restart();
while (!done_ && io_.run_one()) {
} }
} }
void PortScanner::cancelTimer(SteadyTimer &t) void PortScanner::onConnect(Probe *pr, const ErrorCode &ec)
{ {
ErrorCode ec; pr->timer.cancel();
t.cancel();
if (ec) { // Open if connect succeeded; REFUSED means closed but host is alive.
Logger::warn(std::string("cancelTimer: ") + ec.message()); if (!ec) {
openPorts_.push_back(pr->port);
Logger::info(std::string("Open port: ") + std::to_string(pr->port));
} }
ErrorCode ignored;
pr->socket.close(ignored);
finishProbe(pr);
} }
void PortScanner::drainLeftovers() void PortScanner::finishProbe(Probe *pr)
{ {
io_.restart(); if (outstanding_ > 0) {
while (io_.run_one()) { outstanding_ -= 1;
continue;
} }
}
void PortScanner::close(TcpSocket &s) // Remove 'pr' from active_ (active_ size <= kMaxConcurrency, linear
{ // erase is fine).
ErrorCode ig; auto it = std::find_if(
if (s.close(ig)) { active_.begin(), active_.end(), [pr](const std::unique_ptr<Probe> &u) {
Logger::warn(std::string("close socket failed: ") + ig.message()); return u.get() == pr;
}
);
if (it != active_.end()) {
active_.erase(it);
}
if (nextPort_ <= kLastPort) {
startNext();
}
if (outstanding_ == 0 && nextPort_ > kLastPort) {
io_.stop();
return;
} }
} }
+27 -21
View File
@@ -1,37 +1,43 @@
#pragma once #pragma once
#include "aliases.hpp" #include "aliases.hpp"
#include <boost/asio.hpp>
#include <cstdint> #include <cstdint>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
class PortScanner class PortScanner
{ {
public: public:
PortScanner(const std::string &ip, const std::vector<uint16_t> &ports); explicit PortScanner(std::string ip, int timeoutMs = 250);
void setTimeoutMs(int ms); void run(); // blocking scan
const std::vector<uint16_t> &openPorts() const;
void run();
private: private:
bool probe(uint16_t port); struct Probe {
TcpEndpoint endpoint(uint16_t port) const; uint16_t port{0};
TcpSocket socket;
SteadyTimer timer;
void armTimer(SteadyTimer &t, TcpSocket &s); Probe(IoContext &io, uint16_t p);
void armConnect(TcpSocket &s, const TcpEndpoint &ep); };
void runUntilDone();
void cancelTimer(SteadyTimer &t);
void drainLeftovers();
void close(TcpSocket &s);
private: void startNext();
void startProbe(uint16_t port);
void onConnect(Probe *pr, const ErrorCode &ec);
void finishProbe(Probe *pr);
// Fixed settings
static constexpr uint16_t kFirstPort = 1;
static constexpr uint16_t kLastPort = 65535;
static constexpr std::size_t kMaxConcurrency = 256;
// State
std::string ip_;
IoContext io_; IoContext io_;
IpAddress addr_; std::vector<uint16_t> openPorts_;
std::vector<uint16_t> ports_; std::vector<std::unique_ptr<Probe>> active_; // <= kMaxConcurrency
uint32_t nextPort_{kFirstPort};
int timeout_ms_{300}; std::size_t outstanding_{0};
bool ip_ok_{false}; int timeoutMs_;
bool ok_{false};
bool done_{false};
}; };
+41 -18
View File
@@ -1,20 +1,8 @@
#include "cli_base.hpp" #include "local_ping_scanner.hpp"
#include "logger.hpp" #include "logger.hpp"
#include "port_scanner.hpp" #include "port_scanner.hpp"
#include "portscan_cli.hpp" #include "portscan_cli.hpp"
#include <string> #include <string>
#include <vector>
static std::vector<uint16_t> allPorts()
{
std::vector<uint16_t> v;
v.reserve(65535);
for (uint32_t p = 1; p <= 65535; ++p) {
v.push_back(static_cast<uint16_t>(p));
}
return v;
}
int main(int argc, char **argv) int main(int argc, char **argv)
{ {
@@ -27,12 +15,47 @@ int main(int argc, char **argv)
return 0; return 0;
} }
const std::string ip = cli.getIp(); const std::string baseIp = cli.getBaseIp();
auto ports = allPorts();
const int prefix = cli.getPrefix();
if (prefix < 0 || prefix > 32) {
Logger::fatal("Prefix must be in range 0..32\n");
return 2;
}
LocalPingScanner scanner(baseIp, prefix);
Logger::info(std::format("Scanning subnet: {}/{}", baseIp, prefix));
Logger::info("Starting full port scan (1..65535)...");
PortScanner scanner(ip, ports);
scanner.run(); scanner.run();
Logger::info("Scan completed.");
const auto &alive = scanner.alive();
if (alive.empty()) {
Logger::info("No alive hosts found.\n");
return 0;
}
std::cout << "Alive hosts:\n";
for (const auto &ip : alive) {
std::cout << " " << ip << "\n";
}
// For each alive host, run full TCP port scan
for (const auto &ip : alive) {
std::cout << "Scanning ports on " << ip << " ...\n";
PortScanner ps(ip);
ps.run();
const auto &open = ps.openPorts();
if (open.empty()) {
std::cout << " No open TCP ports found on " << ip << "\n";
} else {
std::cout << " Open ports on " << ip << ":\n";
for (uint16_t port : open) {
std::cout << " " << port << "\n";
}
}
}
return 0; return 0;
} }
+9 -1
View File
@@ -10,10 +10,18 @@ add_requires("cmake::Boost", {
}, },
}) })
add_requires("cmake::Poco", {
alias = "poco",
system = true,
configs = {
components = { "Net", "Foundation" },
},
})
target "port_scanner" target "port_scanner"
do do
set_kind "binary" set_kind "binary"
add_files("src/main.cpp", "src/cli/*.cpp", "src/common/*.cpp", "src/core/*.cpp") add_files("src/main.cpp", "src/cli/*.cpp", "src/common/*.cpp", "src/core/*.cpp")
add_includedirs("src", "src/cli", "src/common", "src/core", { public = true }) add_includedirs("src", "src/cli", "src/common", "src/core", { public = true })
add_packages "boost" add_packages("boost", "poco")
end end