Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7efd5cbce5 | |||
| 0e8d77971d | |||
| 025f49a5fd | |||
| 902959f658 | |||
| fcbf6c740c | |||
| 526b27deaf | |||
| 7c19123da9 | |||
| c9e1b0a149 | |||
| 0d96c21199 | |||
| 679d752177 | |||
| 9f88390b1a | |||
| a4c84379ff | |||
| 827cc07175 | |||
| ba91359db3 |
@@ -2,3 +2,4 @@
|
||||
.xmake
|
||||
build
|
||||
compile_commands.json
|
||||
ignore
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
@@ -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"};
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
// --- Error handling ---
|
||||
using ErrorCode = boost::system::error_code;
|
||||
|
||||
// --- Asio types ---
|
||||
using IoContext = boost::asio::io_context;
|
||||
using SteadyTimer = boost::asio::steady_timer;
|
||||
|
||||
// --- Ip types ---
|
||||
using IpAddress = boost::asio::ip::address;
|
||||
|
||||
// --- TCP types ---
|
||||
using TcpSocket = boost::asio::ip::tcp::socket;
|
||||
using TcpAcceptor = boost::asio::ip::tcp::acceptor;
|
||||
using TcpEndpoint = boost::asio::ip::tcp::endpoint;
|
||||
|
||||
// --- UDP types ---
|
||||
using UdpSocket = boost::asio::ip::udp::socket;
|
||||
using UdpEndpoint = boost::asio::ip::udp::endpoint;
|
||||
|
||||
// --- Program options ---
|
||||
using OptionsDescription = boost::program_options::options_description;
|
||||
using VariablesMap = boost::program_options::variables_map;
|
||||
@@ -0,0 +1,34 @@
|
||||
#include "cli_base.hpp"
|
||||
#include <boost/program_options/parsers.hpp>
|
||||
#include <iostream>
|
||||
|
||||
CliBase::CliBase(const std::string &title)
|
||||
: desc_(title)
|
||||
, help_(false)
|
||||
{
|
||||
}
|
||||
|
||||
void CliBase::parse(int argc, char *argv[])
|
||||
{
|
||||
using namespace boost::program_options;
|
||||
|
||||
setupOptions(); // must be first
|
||||
|
||||
parsed_options parsed = parse_command_line(argc, argv, desc_);
|
||||
store(parsed, vm_);
|
||||
notify(vm_);
|
||||
|
||||
if (vm_.count("help")) {
|
||||
help_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool CliBase::isHelp() const
|
||||
{
|
||||
return help_;
|
||||
}
|
||||
|
||||
void CliBase::printHelp() const
|
||||
{
|
||||
std::cout << desc_ << "\n";
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/program_options/options_description.hpp>
|
||||
#include <boost/program_options/variables_map.hpp>
|
||||
#include <string>
|
||||
|
||||
class CliBase
|
||||
{
|
||||
public:
|
||||
CliBase(const std::string &title);
|
||||
|
||||
virtual ~CliBase() = default;
|
||||
|
||||
// parse argc/argv
|
||||
void parse(int argc, char *argv[]);
|
||||
|
||||
bool isHelp() const;
|
||||
void printHelp() const;
|
||||
|
||||
protected:
|
||||
// children add their specific options here
|
||||
virtual void setupOptions() = 0;
|
||||
|
||||
boost::program_options::options_description desc_;
|
||||
boost::program_options::variables_map vm_;
|
||||
bool help_;
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "logger.hpp"
|
||||
|
||||
#include <boost/date_time/posix_time/posix_time_io.hpp>
|
||||
|
||||
void Logger::init(bool toConsole, const std::string_view filePath)
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/program_options.hpp>
|
||||
|
||||
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:
|
||||
OptionUtils() = default;
|
||||
~OptionUtils() = default;
|
||||
};
|
||||
|
||||
#include "option_utils.tpp"
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "option_utils.hpp"
|
||||
|
||||
template <typename T>
|
||||
OptionUtils::OptionValue<T>
|
||||
OptionUtils::withDefault(const T &value, const char *name)
|
||||
{
|
||||
auto v = boost::program_options::value<T>()->default_value(value);
|
||||
if (name) {
|
||||
v->value_name(name); // Optional
|
||||
}
|
||||
return v;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// Alias for raw byte buffer
|
||||
using ByteBuffer = std::vector<std::uint8_t>;
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Sequential file chunk index
|
||||
using ChunkIndex = std::uint32_t;
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// File total size in bytes
|
||||
using FileSize = std::uint64_t;
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Wire length prefix type for a single framed message (big-endian on the wire)
|
||||
using FrameLength = std::uint32_t;
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Client, group, or message ID
|
||||
using Id = std::uint64_t;
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
// IO Context
|
||||
using IoContext = boost::asio::io_context;
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
// TCP
|
||||
using TcpSocket = boost::asio::ip::tcp::socket;
|
||||
using TcpAcceptor = boost::asio::ip::tcp::acceptor;
|
||||
using TcpEndpoint = boost::asio::ip::tcp::endpoint;
|
||||
using TcpResolver = boost::asio::ip::tcp::resolver;
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/steady_timer.hpp>
|
||||
|
||||
// Timers
|
||||
using SteadyTimer = boost::asio::steady_timer;
|
||||
@@ -1,6 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <boost/system/error_code.hpp>
|
||||
|
||||
// Error handling
|
||||
using ErrorCode = boost::system::error_code;
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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_;
|
||||
};
|
||||
+56
-11
@@ -1,16 +1,61 @@
|
||||
#include <boost/asio.hpp>
|
||||
#include <iostream>
|
||||
#include "local_ping_scanner.hpp"
|
||||
#include "logger.hpp"
|
||||
#include "port_scanner.hpp"
|
||||
#include "portscan_cli.hpp"
|
||||
#include <string>
|
||||
|
||||
int main()
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
std::cout << "Hello, Boost + C++23 + xmake!\n";
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boost::asio::io_context io;
|
||||
boost::asio::steady_timer timer(io, std::chrono::seconds(1));
|
||||
timer.async_wait([](const boost::system::error_code &) {
|
||||
std::cout << "Timer fired after 1 second!\n";
|
||||
});
|
||||
|
||||
io.run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,26 @@ set_languages "c++23"
|
||||
|
||||
add_rules("plugin.compile_commands.autoupdate", { outputdir = "." })
|
||||
|
||||
add_requires("boost", {
|
||||
system = true, -- sudo pacman -S boost
|
||||
|
||||
add_requires("cmake::Boost", {
|
||||
alias = "boost",
|
||||
system = true,
|
||||
configs = {
|
||||
all = false,
|
||||
system = true, -- asio
|
||||
components = { "program_options", "log" },
|
||||
},
|
||||
})
|
||||
|
||||
target "app"
|
||||
add_requires("cmake::Poco", {
|
||||
alias = "poco",
|
||||
system = true,
|
||||
configs = {
|
||||
components = { "Net", "Foundation" },
|
||||
},
|
||||
})
|
||||
|
||||
target "port_scanner"
|
||||
do
|
||||
set_kind "binary"
|
||||
add_files "src/*.cpp"
|
||||
add_links "boost_system"
|
||||
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_packages("boost", "poco")
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user