11 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
Ваше Имя 526b27deaf chore(aliases): remove SteadyTimer alias 2025-10-07 18:37:31 +04:00
Ваше Имя 7c19123da9 chore(.gitignore): add ignore file 2025-10-07 18:36:54 +04:00
Ваше Имя c9e1b0a149 feat(portscanner): implement port scanning CLI and core logic
Implement command-line interface for port scanning with IP validation and option
parsing. Add core scanning logic using Boost.Asio for asynchronous socket
checks, including timeout handling and port probing. Update xmake.lua to
configure build target and include necessary files.
2025-10-06 00:05:11 +04:00
Ваше Имя 0d96c21199 feat(common): add Boost Asio aliases
Introduce aliases for Asio IP, TCP, UDP, and timer types to enhance code
readability and structure.
2025-10-06 00:04:34 +04:00
Ваше Имя 679d752177 refactor(common): remove session and tcp server base classes 2025-10-06 00:03:10 +04:00
Ваше Имя 9f88390b1a refactor(option_utils): enhance option handling with default values and names
Removed server.hpp as part of refactoring the option utilities to use Boost's
program_options more effectively, allowing for named default values.
2025-10-05 14:20:21 +04:00
17 changed files with 452 additions and 164 deletions
+1
View File
@@ -2,3 +2,4 @@
.xmake .xmake
build build
compile_commands.json compile_commands.json
ignore
+28
View File
@@ -0,0 +1,28 @@
#include "portscan_cli.hpp"
#include <boost/program_options/value_semantic.hpp>
#include <cassert>
PortScanCli::PortScanCli()
: CliBase("Port Scanner Options")
{
}
void PortScanCli::setupOptions()
{
// clang-format off
desc_.add_options()
("help,h", "Show help")
("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
}
int PortScanCli::getPrefix() const
{
return vm_.at("prefix").as<int>();
}
std::string PortScanCli::getBaseIp() const
{
return vm_.at("ip").as<std::string>();
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "cli_base.hpp"
#include "option_utils.hpp"
#include <string>
class PortScanCli : public CliBase, protected OptionUtils
{
public:
PortScanCli();
int getPrefix() const;
std::string getBaseIp() const;
protected:
void setupOptions() override;
private:
int prefix_{24};
std::string ip_{"10.0.1.0"};
};
+8 -6
View File
@@ -1,27 +1,29 @@
#pragma once #pragma once
#include <boost/asio.hpp> #include <boost/asio.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/program_options.hpp> #include <boost/program_options.hpp>
#include <boost/system/error_code.hpp> #include <boost/system/error_code.hpp>
// --- Error handling --- // --- Error handling ---
using ErrorCode = boost::system::error_code; using ErrorCode = boost::system::error_code;
// --- IO Context --- // --- Asio types ---
using IoContext = boost::asio::io_context; using IoContext = boost::asio::io_context;
using SteadyTimer = boost::asio::steady_timer;
// --- TCP --- // --- Ip types ---
using IpAddress = boost::asio::ip::address;
// --- TCP types ---
using TcpSocket = boost::asio::ip::tcp::socket; using TcpSocket = boost::asio::ip::tcp::socket;
using TcpAcceptor = boost::asio::ip::tcp::acceptor; using TcpAcceptor = boost::asio::ip::tcp::acceptor;
using TcpEndpoint = boost::asio::ip::tcp::endpoint; using TcpEndpoint = boost::asio::ip::tcp::endpoint;
// --- UDP (для DNS) --- // --- UDP types ---
using UdpSocket = boost::asio::ip::udp::socket; using UdpSocket = boost::asio::ip::udp::socket;
using UdpEndpoint = boost::asio::ip::udp::endpoint; using UdpEndpoint = boost::asio::ip::udp::endpoint;
// --- Таймеры ---
using SteadyTimer = boost::asio::steady_timer;
// --- Program options --- // --- Program options ---
using OptionsDescription = boost::program_options::options_description; using OptionsDescription = boost::program_options::options_description;
using VariablesMap = boost::program_options::variables_map; using VariablesMap = boost::program_options::variables_map;
+8 -2
View File
@@ -4,11 +4,17 @@
class OptionUtils class OptionUtils
{ {
public:
template <typename T>
using OptionValue = boost::program_options::typed_value<T> *;
template <typename T>
static OptionValue<T>
withDefault(const T &value, const char *name = nullptr);
protected: protected:
OptionUtils() = default; OptionUtils() = default;
~OptionUtils() = default; ~OptionUtils() = default;
template <typename T> static auto withDefault(T value);
}; };
#include "option_utils.tpp" #include "option_utils.tpp"
+8 -3
View File
@@ -1,8 +1,13 @@
#pragma once #pragma once
#include "option_utils.hpp" #include "option_utils.hpp"
template <typename T> auto OptionUtils::withDefault(T value) template <typename T>
OptionUtils::OptionValue<T>
OptionUtils::withDefault(const T &value, const char *name)
{ {
return boost::program_options::value<T>()->default_value(value); auto v = boost::program_options::value<T>()->default_value(value);
if (name) {
v->value_name(name); // Optional
}
return v;
} }
-9
View File
@@ -1,9 +0,0 @@
#pragma once
class Server
{
public:
virtual ~Server() = default;
virtual void start() = 0;
virtual void stop() = 0;
};
-70
View File
@@ -1,70 +0,0 @@
#include "session_base.hpp"
#include "logger.hpp"
#include <boost/asio/write.hpp>
SessionBase::SessionBase(TcpSocket socket)
: socket_(std::move(socket))
{
}
void SessionBase::start()
{
doRead();
}
void SessionBase::doRead()
{
using namespace boost::asio;
auto self = shared_from_this();
auto handler = [this, self](ErrorCode ec, std::size_t bytesTransferred) {
onRead(ec, bytesTransferred);
};
socket_.async_read_some(buffer(buffer_), handler);
}
void SessionBase::onRead(ErrorCode ec, std::size_t bytesTransferred)
{
if (ec) {
Logger::error("Read error: " + ec.message());
return;
}
std::string rawRequest(buffer_.data(), bytesTransferred);
Logger::info("Received request:\n" + rawRequest);
try {
handleRequest(rawRequest);
} catch (const std::exception &ex) {
Logger::error(std::string("Request handling failed: ") + ex.what());
}
}
void SessionBase::doWrite(const std::string &response)
{
using namespace boost::asio;
auto self = shared_from_this();
auto onWriteHandler =
[this, self](ErrorCode ec, std::size_t /*bytesTransferred*/) {
if (ec) {
Logger::error("Write error: " + ec.message());
}
ErrorCode shutdownEc;
if (socket_.shutdown(TcpSocket::shutdown_both, shutdownEc)) {
Logger::warn("Shutdown error: " + shutdownEc.message());
}
ErrorCode closeEc;
if (socket_.close(closeEc)) {
Logger::warn("Close error: " + closeEc.message());
} else {
Logger::info("Connection closed");
}
};
async_write(socket_, buffer(response), onWriteHandler);
}
-25
View File
@@ -1,25 +0,0 @@
#pragma once
#include "aliases.hpp"
#include <array>
#include <memory>
#include <string>
class SessionBase : public std::enable_shared_from_this<SessionBase>
{
public:
explicit SessionBase(TcpSocket socket);
virtual ~SessionBase() = default;
void start();
protected:
virtual void handleRequest(const std::string_view rawRequest) = 0;
void doRead();
void onRead(ErrorCode ec, std::size_t bytesTransferred);
void doWrite(const std::string &response);
TcpSocket socket_;
std::array<char, 8192> buffer_;
};
-23
View File
@@ -1,23 +0,0 @@
#include "tcp_server_base.hpp"
#include "logger.hpp"
TcpServerBase::TcpServerBase(IoContext &io, unsigned short port)
: acceptor_(io, TcpEndpoint(boost::asio::ip::tcp::v4(), port))
{
}
void TcpServerBase::start()
{
auto doAccept = [this]() {
acceptor_.async_accept([this](ErrorCode ec, TcpSocket socket) {
if (!ec) {
Logger::info("New client connected");
createSession(std::move(socket));
} else {
Logger::error("Accept error: " + ec.message());
}
start(); // continue accepting
});
};
doAccept();
}
-17
View File
@@ -1,17 +0,0 @@
#pragma once
#include "aliases.hpp"
class TcpServerBase
{
public:
TcpServerBase(IoContext &io, unsigned short port);
virtual ~TcpServerBase() = default;
void start();
protected:
virtual void createSession(TcpSocket socket) = 0;
TcpAcceptor acceptor_;
};
+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_;
};
+126
View File
@@ -0,0 +1,126 @@
#include "port_scanner.hpp"
#include "logger.hpp"
#include <algorithm>
PortScanner::PortScanner(std::string ip, int timeoutMs)
: ip_(std::move(ip))
, timeoutMs_(timeoutMs)
{
if (timeoutMs_ <= 0) {
timeoutMs_ = 250;
}
}
PortScanner::Probe::Probe(IoContext &io, uint16_t p)
: port(p)
, socket(io)
, timer(io)
{
}
const std::vector<uint16_t> &PortScanner::openPorts() const
{
return openPorts_;
}
void PortScanner::run()
{
openPorts_.clear();
nextPort_ = kFirstPort;
outstanding_ = 0;
active_.clear();
while (outstanding_ < kMaxConcurrency && nextPort_ <= kLastPort) {
startNext();
}
io_.run();
}
void PortScanner::startNext()
{
if (nextPort_ > kLastPort) {
return;
}
const uint16_t port = static_cast<uint16_t>(nextPort_);
nextPort_ += 1;
startProbe(port);
}
void PortScanner::startProbe(uint16_t port)
{
// Lifetime of Probe is managed in 'active_' until probe finishes.
auto prPtr = std::make_unique<Probe>(io_, port);
Probe *pr = prPtr.get();
active_.push_back(std::move(prPtr));
outstanding_ += 1;
{
using namespace boost::asio::ip;
TcpEndpoint ep(make_address_v4(ip_), port);
pr->timer.expires_after(std::chrono::milliseconds(timeoutMs_));
auto onTimer = [pr](const ErrorCode &ec) {
// Cancel socket on timeout; completion will run onConnect with
// ec=operation_aborted.
if (!ec) {
pr->socket.cancel();
}
};
auto onAsyncConnect = [this, pr](const ErrorCode &ec) {
onConnect(pr, ec);
};
pr->timer.async_wait(onTimer);
pr->socket.async_connect(ep, onAsyncConnect);
}
}
void PortScanner::onConnect(Probe *pr, const ErrorCode &ec)
{
pr->timer.cancel();
// Open if connect succeeded; REFUSED means closed but host is alive.
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::finishProbe(Probe *pr)
{
if (outstanding_ > 0) {
outstanding_ -= 1;
}
// Remove 'pr' from active_ (active_ size <= kMaxConcurrency, linear
// erase is fine).
auto it = std::find_if(
active_.begin(), active_.end(), [pr](const std::unique_ptr<Probe> &u) {
return u.get() == pr;
}
);
if (it != active_.end()) {
active_.erase(it);
}
if (nextPort_ <= kLastPort) {
startNext();
}
if (outstanding_ == 0 && nextPort_ > kLastPort) {
io_.stop();
return;
}
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "aliases.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
class PortScanner
{
public:
explicit PortScanner(std::string ip, int timeoutMs = 250);
void run(); // blocking scan
const std::vector<uint16_t> &openPorts() const;
private:
struct Probe {
uint16_t port{0};
TcpSocket socket;
SteadyTimer timer;
Probe(IoContext &io, uint16_t p);
};
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_;
std::vector<uint16_t> openPorts_;
std::vector<std::unique_ptr<Probe>> active_; // <= kMaxConcurrency
uint32_t nextPort_{kFirstPort};
std::size_t outstanding_{0};
int timeoutMs_;
};
+61
View File
@@ -0,0 +1,61 @@
#include "local_ping_scanner.hpp"
#include "logger.hpp"
#include "port_scanner.hpp"
#include "portscan_cli.hpp"
#include <string>
int main(int argc, char **argv)
{
Logger::init(true);
PortScanCli cli;
cli.parse(argc, argv);
if (cli.isHelp()) {
cli.printHelp();
return 0;
}
const std::string baseIp = cli.getBaseIp();
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));
scanner.run();
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;
}
+18 -8
View File
@@ -2,16 +2,26 @@ set_languages "c++23"
add_rules("plugin.compile_commands.autoupdate", { outputdir = "." }) add_rules("plugin.compile_commands.autoupdate", { outputdir = "." })
add_requires("boost", { add_requires("cmake::Boost", {
system = true, -- sudo pacman -S boost alias = "boost",
system = true,
configs = { configs = {
all = false, components = { "program_options", "log" },
system = true, -- asio
}, },
}) })
target "app" add_requires("cmake::Poco", {
alias = "poco",
system = true,
configs = {
components = { "Net", "Foundation" },
},
})
target "port_scanner"
do
set_kind "binary" set_kind "binary"
add_files "src/*.cpp" add_files("src/main.cpp", "src/cli/*.cpp", "src/common/*.cpp", "src/core/*.cpp")
add_links "boost_system" add_includedirs("src", "src/cli", "src/common", "src/core", { public = true })
add_packages("boost", "poco")
end