From ebb3bc0d84353ca1981d2e673de180abecbfc359 Mon Sep 17 00:00:00 2001 From: Lee Yunjin Date: Thu, 25 Jun 2026 22:58:03 +0900 Subject: [PATCH] impl: write ICMP socket in Qt style --- RLib-core/examples/CMakeLists.txt | 5 + RLib-core/examples/licmpsocket_ping.cpp | 80 +++++ RLib-core/src/CMakeLists.txt | 2 + RLib-core/src/LIcmpSocket.cpp | 418 ++++++++++++++++++++++++ RLib-core/src/LIcmpSocket.hpp | 120 +++++++ RLib-core/tests/CMakeLists.txt | 1 + RLib-core/tests/test_licmpsocket.cpp | 52 +++ 7 files changed, 678 insertions(+) create mode 100644 RLib-core/examples/licmpsocket_ping.cpp create mode 100644 RLib-core/src/LIcmpSocket.cpp create mode 100644 RLib-core/src/LIcmpSocket.hpp create mode 100644 RLib-core/tests/test_licmpsocket.cpp diff --git a/RLib-core/examples/CMakeLists.txt b/RLib-core/examples/CMakeLists.txt index fceee71..3fb756b 100644 --- a/RLib-core/examples/CMakeLists.txt +++ b/RLib-core/examples/CMakeLists.txt @@ -10,11 +10,13 @@ add_executable(core-01-timer ltimer_exmaple.cpp) add_executable(core-02-udp_echo_server ludpsocket_udpechoserver.cpp) add_executable(core-03-udp_sender ludpsocket_udpsender.cpp) add_executable(core-04-tcp_echo_server tcp_echo_server.cpp) +add_executable(core-05-icmp_ping licmpsocket_ping.cpp) target_link_libraries(core-01-timer PRIVATE RLib) target_link_libraries(core-02-udp_echo_server PRIVATE RLib) target_link_libraries(core-03-udp_sender PRIVATE RLib) target_link_libraries(core-04-tcp_echo_server PRIVATE RLib) +target_link_libraries(core-05-icmp_ping PRIVATE RLib) if(CMAKE_BUILD_TYPE MATCHES "Debug") @@ -35,4 +37,7 @@ if(CMAKE_BUILD_TYPE MATCHES "Debug") target_compile_options(core-04-tcp_echo_server PRIVATE -fsanitize=address -g -fno-omit-frame-pointer) target_link_options(core-04-tcp_echo_server PRIVATE -fsanitize=address) + + target_compile_options(core-05-icmp_ping PRIVATE -fsanitize=address -g -fno-omit-frame-pointer) + target_link_options(core-05-icmp_ping PRIVATE -fsanitize=address) endif() diff --git a/RLib-core/examples/licmpsocket_ping.cpp b/RLib-core/examples/licmpsocket_ping.cpp new file mode 100644 index 0000000..787b689 --- /dev/null +++ b/RLib-core/examples/licmpsocket_ping.cpp @@ -0,0 +1,80 @@ +#include +#include +#include + +#include "LEventLoop.hpp" +#include "LIcmpSocket.hpp" +#include "LTimer.hpp" + +class Pinger +{ +public: + Pinger() + : m_sequence(0) + { + if (socket.error() != LIcmpSocket::UnknownSocketError) { + std::cerr << "Failed to open ICMP socket: " << socket.errorString() << std::endl; + std::cerr << "(raw ICMP sockets usually require root privileges or CAP_NET_RAW)" + << std::endl; + return; + } + + socket.onReadyRead(this, &Pinger::readPendingReplies); + + sendPing(); + timer.onTimeout(this, &Pinger::sendPing); + timer.start(1000); + } + + void sendPing() + { + ++m_sequence; + std::string payload = "RLib ICMP ping test"; + + int64_t bytes = socket.ping("127.0.0.1", + 0x4242, + m_sequence, + std::vector(payload.begin(), payload.end())); + + if (bytes > 0) { + std::cout << "Ping #" << m_sequence << " sent (" << bytes << " bytes)" << std::endl; + } else { + std::cerr << "Failed to send ping: " << socket.errorString() << std::endl; + } + } + + void readPendingReplies() + { + LIcmpSocket::EchoReply reply; + while (socket.readEchoReply(&reply)) { + std::cout << "Reply from " << reply.address + << ": id=" << reply.id + << " seq=" << reply.sequence; + + if (!reply.payload.empty()) { + std::string text(reply.payload.begin(), reply.payload.end()); + std::cout << " payload=\"" << text << "\""; + } + std::cout << std::endl; + } + } + +private: + LIcmpSocket socket; + LTimer timer; + uint16_t m_sequence; +}; + +// Sends ICMP Echo Requests to 127.0.0.1 every second and prints replies. +// Run with root privileges or CAP_NET_RAW: +// +// sudo ./core-05-icmp_ping +// +int main() +{ + LEventLoop loop; + + Pinger pinger; + + return loop.exec(); +} diff --git a/RLib-core/src/CMakeLists.txt b/RLib-core/src/CMakeLists.txt index de55c88..a6991a8 100644 --- a/RLib-core/src/CMakeLists.txt +++ b/RLib-core/src/CMakeLists.txt @@ -16,6 +16,7 @@ add_library(RLib STATIC LTaskQueue.hpp LUnbufferedTcpSocket.hpp LUdpSocket.hpp + LIcmpSocket.hpp LByteRingBuffer.cpp LTimer.cpp @@ -27,6 +28,7 @@ add_library(RLib STATIC LTaskQueue.cpp LUnbufferedTcpSocket.cpp LUdpSocket.cpp + LIcmpSocket.cpp ) target_include_directories(RLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/RLib-core/src/LIcmpSocket.cpp b/RLib-core/src/LIcmpSocket.cpp new file mode 100644 index 0000000..0f952b4 --- /dev/null +++ b/RLib-core/src/LIcmpSocket.cpp @@ -0,0 +1,418 @@ +#include "LIcmpSocket.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +struct IcmpEchoHeader { + uint8_t type; + uint8_t code; + uint16_t checksum; + uint16_t id; + uint16_t sequence; +}; + +uint16_t calculateChecksum(const uint16_t *data, size_t length) +{ + uint32_t sum = 0; + + while (length > 1) { + sum += *data++; + length -= sizeof(uint16_t); + } + + if (length == 1) { + sum += *reinterpret_cast(data); + } + + while (sum >> 16) { + sum = (sum & 0xFFFF) + (sum >> 16); + } + + return static_cast(~sum); +} + +} + +LIcmpSocket::LIcmpSocket() + : m_socket_fd(-1) + , m_state(UnconnectedState) + , m_error(UnknownSocketError) +{ + std::cout << __PRETTY_FUNCTION__ << std::endl; + + m_socket_fd = ::socket(AF_INET, SOCK_RAW | SOCK_NONBLOCK | SOCK_CLOEXEC, IPPROTO_ICMP); + + if (m_socket_fd == -1) { + if (errno == EPERM || errno == EACCES) { + setError(SocketAccessError, + "Permission denied creating raw ICMP socket (requires root / CAP_NET_RAW)"); + } else { + setError(SocketResourceError, strerror(errno)); + } + return; + } + + LEventLoop *loop = LEventLoop::current(); + if (loop) { + loop->registerHandler(m_socket_fd, EPOLLIN, this); + } else { + std::cerr << "LIcmpSocket Warning: No LEventLoop in current thread!" << std::endl; + } +} + +LIcmpSocket::~LIcmpSocket() +{ + if (m_socket_fd != -1) { + LEventLoop *loop = LEventLoop::current(); + if (loop) { + loop->unregisterHandler(m_socket_fd); + } + ::close(m_socket_fd); + m_socket_fd = -1; + } + + m_readyReadCallback = nullptr; + m_bytesWrittenCallback = nullptr; + m_errorCallback = nullptr; + m_stateCallback = nullptr; + + std::cout << __PRETTY_FUNCTION__ << std::endl; +} + +bool LIcmpSocket::bind(const std::string &address) +{ + if (m_socket_fd == -1) + return false; + + struct sockaddr_in addr; + std::memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + + if (inet_pton(AF_INET, address.c_str(), &addr.sin_addr) <= 0) { + setError(NetworkError, "Invalid IP address format: " + address); + return false; + } + + if (::bind(m_socket_fd, reinterpret_cast(&addr), sizeof(addr)) == -1) { + if (errno == EADDRINUSE) { + setError(AddressInUseError, strerror(errno)); + } else { + setError(SocketAccessError, strerror(errno)); + } + return false; + } + + setState(BoundState); + return true; +} + +void LIcmpSocket::connectToHost(const std::string &hostName) +{ + if (m_socket_fd == -1) + return; + + struct sockaddr_in addr; + std::memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + + if (inet_pton(AF_INET, hostName.c_str(), &addr.sin_addr) <= 0) { + setError(NetworkError, "Invalid IP address: " + hostName); + return; + } + + if (::connect(m_socket_fd, reinterpret_cast(&addr), sizeof(addr)) == -1) { + setError(NetworkError, strerror(errno)); + return; + } + + m_peerAddress = hostName; + setState(ConnectedState); +} + +void LIcmpSocket::disconnectFromHost() +{ + if (m_socket_fd == -1) + return; + + struct sockaddr_in unspec; + std::memset(&unspec, 0, sizeof(unspec)); + unspec.sin_family = AF_UNSPEC; + ::connect(m_socket_fd, reinterpret_cast(&unspec), sizeof(unspec)); + + m_peerAddress.clear(); + setState(UnconnectedState); +} + +void LIcmpSocket::abort() +{ + disconnectFromHost(); +} + +bool LIcmpSocket::hasPendingDatagrams() const +{ + return pendingDatagramSize() > 0; +} + +int64_t LIcmpSocket::pendingDatagramSize() const +{ + if (m_socket_fd == -1) + return -1; + + int bytes_available = 0; + if (ioctl(m_socket_fd, FIONREAD, &bytes_available) == -1) { + return -1; + } + return bytes_available; +} + +int64_t LIcmpSocket::readDatagram(char *data, int64_t maxSize, std::string *address) +{ + if (m_socket_fd == -1) + return -1; + + struct sockaddr_in sender; + socklen_t sender_len = sizeof(sender); + + ssize_t ret = ::recvfrom(m_socket_fd, + data, + maxSize, + 0, + reinterpret_cast(&sender), + &sender_len); + + if (ret > 0) { + if (address) { + char ip_str[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(sender.sin_addr), ip_str, INET_ADDRSTRLEN); + *address = std::string(ip_str); + } + } else if (ret == -1 && errno != EAGAIN && errno != EWOULDBLOCK) { + setError(NetworkError, strerror(errno)); + } + + return ret; +} + +std::vector LIcmpSocket::receiveDatagram(std::string *address) +{ + int64_t size = pendingDatagramSize(); + if (size <= 0) + return {}; + + std::vector buffer(size); + int64_t read_bytes = readDatagram(reinterpret_cast(buffer.data()), size, address); + + if ((read_bytes < size) && (read_bytes > 0)) { + buffer.resize(read_bytes); + } else if (read_bytes <= 0) { + return {}; + } + + return buffer; +} + +int64_t LIcmpSocket::ping(const std::string &address, + uint16_t id, + uint16_t sequence, + const std::vector &payload) +{ + const size_t packetSize = sizeof(IcmpEchoHeader) + payload.size(); + std::vector packet(packetSize, 0); + + IcmpEchoHeader *header = reinterpret_cast(packet.data()); + header->type = ICMP_ECHO; + header->code = 0; + header->id = htons(id); + header->sequence = htons(sequence); + + if (!payload.empty()) { + std::memcpy(packet.data() + sizeof(IcmpEchoHeader), payload.data(), payload.size()); + } + + header->checksum = 0; + header->checksum = calculateChecksum(reinterpret_cast(packet.data()), + packet.size()); + + return writeDatagram(reinterpret_cast(packet.data()), + static_cast(packet.size()), + address); +} + +bool LIcmpSocket::readEchoReply(EchoReply *reply) +{ + if (!reply) + return false; + + while (hasPendingDatagrams()) { + std::string senderAddress; + std::vector datagram = receiveDatagram(&senderAddress); + if (datagram.empty()) + return false; + + if (parseEchoReply(datagram, reply)) { + reply->address = senderAddress; + return true; + } + } + + return false; +} + +int64_t LIcmpSocket::write(const char *data, int64_t size) +{ + if (m_socket_fd == -1 || m_state != ConnectedState) + return -1; + + ssize_t ret = ::send(m_socket_fd, data, size, 0); + + if ((ret > 0) && m_bytesWrittenCallback) { + m_bytesWrittenCallback(ret); + } else if (ret == -1) { + if (errno == EMSGSIZE) + setError(DatagramTooLargeError, strerror(errno)); + else + setError(NetworkError, strerror(errno)); + } + + return ret; +} + +int64_t LIcmpSocket::writeDatagram(const char *data, + int64_t size, + const std::string &address) +{ + if (m_socket_fd == -1) + return -1; + + struct sockaddr_in dest; + std::memset(&dest, 0, sizeof(dest)); + dest.sin_family = AF_INET; + + if (inet_pton(AF_INET, address.c_str(), &dest.sin_addr) <= 0) { + setError(NetworkError, "Invalid destination IP"); + return -1; + } + + ssize_t ret = ::sendto(m_socket_fd, + data, + size, + 0, + reinterpret_cast(&dest), + sizeof(dest)); + + if (ret > 0 && m_bytesWrittenCallback) { + m_bytesWrittenCallback(ret); + } else if (ret == -1) { + if (errno == EMSGSIZE) + setError(DatagramTooLargeError, strerror(errno)); + else + setError(NetworkError, strerror(errno)); + } + + return ret; +} + +LIcmpSocket::SocketState LIcmpSocket::state() const +{ + return m_state; +} + +LIcmpSocket::SocketError LIcmpSocket::error() const +{ + return m_error; +} + +std::string LIcmpSocket::errorString() const +{ + return m_errorString; +} + +void LIcmpSocket::onReadyRead(std::function callback) +{ + m_readyReadCallback = callback; +} + +void LIcmpSocket::onBytesWritten(std::function callback) +{ + m_bytesWrittenCallback = callback; +} + +void LIcmpSocket::onErrorOccurred(std::function callback) +{ + m_errorCallback = callback; +} + +void LIcmpSocket::onStateChanged(std::function callback) +{ + m_stateCallback = callback; +} + +void LIcmpSocket::handleEpollEvent(uint32_t events) +{ + if (events & EPOLLIN) { + if (m_readyReadCallback) { + m_readyReadCallback(); + } + } +} + +void LIcmpSocket::setError(SocketError error, const std::string &errorString) +{ + m_error = error; + m_errorString = errorString; + if (m_errorCallback) { + m_errorCallback(m_error); + } +} + +void LIcmpSocket::setState(SocketState state) +{ + if (m_state != state) { + m_state = state; + if (m_stateCallback) { + m_stateCallback(m_state); + } + } +} + +bool LIcmpSocket::parseEchoReply(const std::vector &datagram, EchoReply *reply) const +{ + if (datagram.size() < sizeof(struct ip)) + return false; + + const struct ip *ipHeader = reinterpret_cast(datagram.data()); + const size_t ipHeaderLen = ipHeader->ip_hl * 4; + + if (datagram.size() < ipHeaderLen + sizeof(IcmpEchoHeader)) + return false; + + const IcmpEchoHeader *icmpHeader = + reinterpret_cast(datagram.data() + ipHeaderLen); + + if (icmpHeader->type != ICMP_ECHOREPLY || icmpHeader->code != 0) + return false; + + const size_t payloadOffset = ipHeaderLen + sizeof(IcmpEchoHeader); + if (datagram.size() > payloadOffset) { + reply->payload.assign(datagram.begin() + payloadOffset, datagram.end()); + } else { + reply->payload.clear(); + } + + reply->id = ntohs(icmpHeader->id); + reply->sequence = ntohs(icmpHeader->sequence); + return true; +} diff --git a/RLib-core/src/LIcmpSocket.hpp b/RLib-core/src/LIcmpSocket.hpp new file mode 100644 index 0000000..572c5da --- /dev/null +++ b/RLib-core/src/LIcmpSocket.hpp @@ -0,0 +1,120 @@ +#pragma once + +#include +#include +#include +#include + +#include "LEventLoop.hpp" + +// ICMP raw socket for IPv4 echo request/reply. +class LIcmpSocket : public LEpollHandler +{ +public: + enum SocketState { + UnconnectedState, + HostLookupState, + ConnectingState, + ConnectedState, + BoundState + }; + + enum SocketError { + UnknownSocketError, + ConnectionRefusedError, + DatagramTooLargeError, + NetworkError, + AddressInUseError, + SocketAccessError, + SocketResourceError + }; + + struct EchoReply { + uint16_t id = 0; + uint16_t sequence = 0; + std::vector payload; + std::string address; + }; + + LIcmpSocket(); + ~LIcmpSocket() override; + + bool bind(const std::string &address); + + void connectToHost(const std::string &hostName); + void disconnectFromHost(); + void abort(); + + bool hasPendingDatagrams() const; + int64_t pendingDatagramSize() const; + + int64_t readDatagram(char *data, + int64_t maxSize, + std::string *address = nullptr); + std::vector receiveDatagram(std::string *address = nullptr); + + int64_t ping(const std::string &address, + uint16_t id, + uint16_t sequence, + const std::vector &payload = {}); + bool readEchoReply(EchoReply *reply); + + int64_t write(const char *data, int64_t size); + int64_t writeDatagram(const char *data, + int64_t size, + const std::string &address); + + SocketState state() const; + SocketError error() const; + std::string errorString() const; + + void onReadyRead(std::function callback); + void onBytesWritten(std::function callback); + void onErrorOccurred(std::function callback); + void onStateChanged(std::function callback); + + template + void onReadyRead(Object *obj, void (Object::*method)()) + { + m_readyReadCallback = [obj, method]() { (obj->*method)(); }; + } + + template + void onBytesWritten(Object *obj, void (Object::*method)(int64_t)) + { + m_bytesWrittenCallback = [obj, method](int64_t bytes) { (obj->*method)(bytes); }; + } + + template + void onErrorOccurred(Object *obj, void (Object::*method)(SocketError)) + { + m_errorCallback = [obj, method](SocketError error) { (obj->*method)(error); }; + } + + template + void onStateChanged(Object *obj, void (Object::*method)(SocketState)) + { + m_stateCallback = [obj, method](SocketState state) { (obj->*method)(state); }; + } + +protected: + void handleEpollEvent(uint32_t events) override; + +private: + void setError(SocketError error, const std::string &errorString); + void setState(SocketState state); + + bool parseEchoReply(const std::vector &datagram, EchoReply *reply) const; + + int m_socket_fd; + SocketState m_state; + SocketError m_error; + std::string m_errorString; + + std::string m_peerAddress; + + std::function m_readyReadCallback; + std::function m_bytesWrittenCallback; + std::function m_errorCallback; + std::function m_stateCallback; +}; diff --git a/RLib-core/tests/CMakeLists.txt b/RLib-core/tests/CMakeLists.txt index 6cfa2ce..5c166ea 100644 --- a/RLib-core/tests/CMakeLists.txt +++ b/RLib-core/tests/CMakeLists.txt @@ -23,6 +23,7 @@ add_executable(RLibTests test_ltimer.cpp test_ludpsocket.cpp test_ltcpsocket.cpp + test_licmpsocket.cpp ) target_link_libraries(RLibTests diff --git a/RLib-core/tests/test_licmpsocket.cpp b/RLib-core/tests/test_licmpsocket.cpp new file mode 100644 index 0000000..7ed7d56 --- /dev/null +++ b/RLib-core/tests/test_licmpsocket.cpp @@ -0,0 +1,52 @@ +#include "LIcmpSocket.hpp" + +#include +#include + +#include +#include + +TEST(LIcmpSocketTest, InitialStateIsUnconnected) +{ + LIcmpSocket socket; + EXPECT_EQ(socket.state(), LIcmpSocket::UnconnectedState); +} + +TEST(LIcmpSocketTest, RawSocketCreatedOrReportsAccessError) +{ + LIcmpSocket socket; + + if (getuid() != 0) { + EXPECT_EQ(socket.error(), LIcmpSocket::SocketAccessError); + } else { + EXPECT_EQ(socket.error(), LIcmpSocket::UnknownSocketError); + EXPECT_EQ(socket.state(), LIcmpSocket::UnconnectedState); + } +} + +TEST(LIcmpSocketTest, LocalEchoRequestAndReply) +{ + if (getuid() != 0) { + GTEST_SKIP() << "Raw ICMP sockets require root privileges; skipping ping test."; + } + + LIcmpSocket socket; + ASSERT_EQ(socket.error(), LIcmpSocket::UnknownSocketError); + + const uint16_t id = 0xABCD; + const uint16_t sequence = 1; + const std::vector payload = { 'R', 'L', 'i', 'b' }; + + int64_t sent = socket.ping("127.0.0.1", id, sequence, payload); + ASSERT_GT(sent, 0); + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + LIcmpSocket::EchoReply reply; + ASSERT_TRUE(socket.readEchoReply(&reply)); + + EXPECT_EQ(reply.id, id); + EXPECT_EQ(reply.sequence, sequence); + EXPECT_EQ(reply.payload, payload); + EXPECT_EQ(reply.address, "127.0.0.1"); +}