This repository has been archived on 2026-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
Simon Stürz 0d20cf7816 Hold reference count of monitor objects
Make host lookup optional
Cleanup pending ping on monitor unregister
2022-06-20 16:58:05 +02:00

486 lines
18 KiB
C++

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright 2013 - 2021, nymea GmbH
* Contact: contact@nymea.io
*
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see <https://www.gnu.org/licenses/>.
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "ping.h"
#include "networkutils.h"
#include "loggingcategories.h"
#include <fcntl.h>
#include <sys/types.h>
#include <resolv.h>
#include <netinet/in.h>
#include <netinet/ip_icmp.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <QtEndian>
NYMEA_LOGGING_CATEGORY(dcPing, "Ping")
NYMEA_LOGGING_CATEGORY(dcPingTraffic, "PingTraffic")
Ping::Ping(QObject *parent) : QObject(parent)
{
m_queueTimer = new QTimer(this);
m_queueTimer->setInterval(20);
m_queueTimer->setSingleShot(true);
connect(m_queueTimer, &QTimer::timeout, this, [=](){
sendNextReply();
});
// Build socket descriptor
m_socketDescriptor = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (m_socketDescriptor < 0) {
qCWarning(dcPing()) << "Failed to create the ICMP socket." << strerror(errno);
verifyErrno(errno);
return;
}
// Set time to live value
const int val = ICMP_TTL_VALUE;
if (setsockopt(m_socketDescriptor, SOL_IP, IP_TTL, &val, sizeof(val)) != 0) {
verifyErrno(errno);
qCWarning(dcPing()) << "Failed to set the ICMP socket TTL option:" << strerror(errno);
cleanUpSocket();
return;
}
// Configure non blocking
if (fcntl(m_socketDescriptor, F_SETFL, fcntl(m_socketDescriptor, F_GETFL, 0) | O_NONBLOCK) != 0) {
verifyErrno(errno);
qCWarning(dcPing()) << "Failed to set the ICMP socket function control to non-blocking" << strerror(errno);
cleanUpSocket();
return;
}
// Create the socket notifier for read notification
m_socketNotifier = new QSocketNotifier(m_socketDescriptor, QSocketNotifier::Read, this);
connect(m_socketNotifier, &QSocketNotifier::activated, this, &Ping::onSocketReadyRead);
m_socketNotifier->setEnabled(true);
m_available = true;
qCDebug(dcPing()) << "ICMP socket set up successfully (Socket ID:" << m_socketDescriptor << ")";
}
QByteArray Ping::payload() const
{
return m_payload;
}
void Ping::setPayload(const QByteArray &payload)
{
Q_ASSERT_X(static_cast<uint>(payload.count()) <= ICMP_PAYLOAD_SIZE, "ping", QString("maximal payload size is %1").arg(ICMP_PAYLOAD_SIZE).toLocal8Bit());
m_payload = payload;
}
bool Ping::available() const
{
return m_available;
}
PingReply::Error Ping::error() const
{
return m_error;
}
PingReply *Ping::ping(const QHostAddress &hostAddress, uint retries)
{
PingReply *reply = createReply(hostAddress);
reply->m_retries = retries;
// Perform the reply in the next event loop to give the user time to do the reply connects
m_replyQueue.enqueue(reply);
sendNextReply();
return reply;
}
PingReply *Ping::ping(const QHostAddress &hostAddress, bool lookupHost, uint retries)
{
PingReply *reply = createReply(hostAddress);
reply->m_retries = retries;
reply->m_doHostLookup = lookupHost;
// Perform the reply in the next event loop to give the user time to do the reply connects
m_replyQueue.enqueue(reply);
sendNextReply();
return reply;
}
void Ping::sendNextReply()
{
if (m_queueTimer->isActive())
return;
if (m_replyQueue.isEmpty())
return;
PingReply *reply = m_replyQueue.dequeue();
qCDebug(dcPing()) << "Send next reply," << m_replyQueue.count() << "left in queue";
m_queueTimer->start();
QTimer::singleShot(0, reply, [=]() { performPing(reply); });
}
void Ping::performPing(PingReply *reply)
{
if (!m_available) {
qCDebug(dcPing()) << "Cannot send ping request" << m_error;
finishReply(reply, m_error);
return;
}
if (reply->targetHostAddress().isNull()) {
m_error = PingReply::ErrorInvalidHostAddress;
qCWarning(dcPing()) << "Cannot send ping request" << m_error;
finishReply(reply, m_error);
return;
}
// Get host ip address
struct hostent *hostname = gethostbyname(reply->targetHostAddress().toString().toLocal8Bit().constData());
struct sockaddr_in pingAddress;
memset(&pingAddress, 0, sizeof(pingAddress));
pingAddress.sin_family = hostname->h_addrtype;
pingAddress.sin_port = 0;
pingAddress.sin_addr.s_addr = *(long*)hostname->h_addr;
QHostAddress targetHostAddress = QHostAddress(qFromBigEndian(pingAddress.sin_addr.s_addr));
// Build the ICMP echo request packet
struct icmpPacket requestPacket;
memset(&requestPacket, 0, sizeof(requestPacket));
requestPacket.icmpHeadr.type = ICMP_ECHO;
if (reply->requestId() == 0) {
requestPacket.icmpHeadr.un.echo.id = calculateRequestId();
} else {
requestPacket.icmpHeadr.un.echo.id = reply->requestId();
}
requestPacket.icmpHeadr.un.echo.sequence = htons(reply->m_sequenceNumber++);
// Write the ICMP payload
memset(&requestPacket.icmpPayload, ' ', sizeof(requestPacket.icmpPayload));
for (int i = 0; i < m_payload.count(); i++)
requestPacket.icmpPayload[i] = m_payload.at(i);
// Calculate the ICMP packet checksum
requestPacket.icmpHeadr.checksum = calculateChecksum(reinterpret_cast<unsigned short *>(&requestPacket), sizeof(requestPacket));
// Get time for ping measurement and fill reply information
if (gettimeofday(&reply->m_startTime, nullptr) < 0 ) {
qCWarning(dcPing()) << "Failed to get start time for ping measurement" << strerror(errno);
}
reply->m_requestId = requestPacket.icmpHeadr.un.echo.id;
reply->m_targetHostAddress = targetHostAddress;
reply->m_sequenceNumber = requestPacket.icmpHeadr.un.echo.sequence;
qCDebug(dcPingTraffic()) << "Send ICMP echo request" << reply->targetHostAddress().toString() << ICMP_PACKET_SIZE << "[Bytes]"
<< "ID:" << QString("0x%1").arg(requestPacket.icmpHeadr.un.echo.id, 4, 16, QChar('0'))
<< "Sequence:" << htons(requestPacket.icmpHeadr.un.echo.sequence);
// Send packet to the target ip
int bytesSent = sendto(m_socketDescriptor, &requestPacket, sizeof(requestPacket), 0, (struct sockaddr *)&pingAddress, sizeof(pingAddress));
if (bytesSent < 0) {
verifyErrno(errno);
qCWarning(dcPing()) << "Failed to send data to" << reply->targetHostAddress().toString() << strerror(errno);
finishReply(reply, m_error);
return;
}
// Start reply timer and handle timeout
m_pendingReplies.insert(reply->requestId(), reply);
reply->m_timer->start(m_timeoutDuration);
}
void Ping::verifyErrno(int error)
{
switch (error) {
case ENETDOWN:
m_error = PingReply::ErrorNetworkDown;
break;
case ENETUNREACH:
m_error = PingReply::ErrorNetworkUnreachable;
break;
case EACCES:
case EPERM:
m_error = PingReply::ErrorPermissionDenied;
break;
default:
m_error = PingReply::ErrorSocketError;
}
}
unsigned short Ping::calculateChecksum(unsigned short *b, int len)
{
unsigned short *buf = b;
unsigned int sum = 0;
unsigned short result;
for (sum = 0; len > 1; len -= 2)
sum += *buf++;
if (len == 1)
sum += *(unsigned char*)buf;
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
result = ~sum;
return result;
}
void Ping::cleanUpSocket()
{
m_available = false;
if (m_socketNotifier) {
m_socketNotifier->setEnabled(false);
delete m_socketNotifier;
m_socketNotifier = nullptr;
}
if (m_socketDescriptor >= 0) {
close(m_socketDescriptor);
m_socketDescriptor = -1;
}
}
void Ping::timeValueSubtract(timeval *start, timeval *stop)
{
int sec = start->tv_sec - stop->tv_sec;
int usec = start->tv_usec - stop->tv_usec;
if (usec < 0) {
start->tv_sec = sec - 1;
start->tv_usec = 1000000 + usec;
} else {
start->tv_sec = sec;
start->tv_usec = usec;
}
}
quint16 Ping::calculateRequestId()
{
quint16 requestId = 0;
while (requestId == 0 || m_pendingReplies.contains(requestId)) {
requestId = rand();
}
return requestId;
}
PingReply *Ping::createReply(const QHostAddress &hostAddress)
{
PingReply *reply = new PingReply(this);
reply->m_targetHostAddress = hostAddress;
reply->m_networkInterface = NetworkUtils::getInterfaceForHostaddress(hostAddress);
connect(reply, &PingReply::timeout, this, [=](){
// Note: this is not the ICMP timeout, here we actually got nothing from nobody...
finishReply(reply, PingReply::ErrorTimeout);
});
connect(reply, &PingReply::aborted, this, [=](){
finishReply(reply, PingReply::ErrorAborted);
});
return reply;
}
void Ping::finishReply(PingReply *reply, PingReply::Error error)
{
// Check if we should retry
if (reply->m_retryCount >= reply->m_retries ||
error == PingReply::ErrorNoError ||
error == PingReply::ErrorAborted ||
error == PingReply::ErrorInvalidHostAddress ||
error == PingReply::ErrorPermissionDenied) {
// No retry, we are done
reply->m_error = error;
reply->m_timer->stop();
m_pendingReplies.remove(reply->requestId());
emit reply->finished();
reply->deleteLater();
} else {
m_pendingReplies.remove(reply->requestId());
reply->m_error = error;
reply->m_retryCount++;
reply->m_sequenceNumber++;
qCDebug(dcPing()) << "Ping finished with error" << error << "Retry ping" << reply->targetHostAddress().toString() << reply->m_retryCount << "/" << reply->m_retries;
emit reply->retry(error, reply->retryCount());
// Note: will be restarted once actually sent trough the network
reply->m_timer->stop();
// Re-Enqueu the reply
m_replyQueue.enqueue(reply);
sendNextReply();
}
}
void Ping::onSocketReadyRead(int socketDescriptor)
{
// We must read all data otherwise the socket notifier does not work as expected
while (true) {
// Read the socket data and give some extra space for nested pakets...
int receiveBufferSize = 2 * ICMP_PACKET_SIZE + sizeof(struct iphdr);
char receiveBuffer[receiveBufferSize];
memset(&receiveBuffer, 0, sizeof(receiveBufferSize));
int bytesReceived = recv(socketDescriptor, &receiveBuffer, receiveBufferSize, 0);
if (bytesReceived < 0) {
return;
}
qCDebug(dcPingTraffic()) << "Received" << bytesReceived << "bytes" << "( Socket ID:" << m_socketDescriptor << ")";
struct iphdr *ipHeader = (struct iphdr *)receiveBuffer;
int ipHeaderLength = ipHeader->ihl << 2;
int icmpPacketSize = htons(ipHeader->tot_len) - ipHeaderLength;
QHostAddress senderAddress(qFromBigEndian(ipHeader->saddr));
QHostAddress destinationAddress(qFromBigEndian(ipHeader->daddr));
qCDebug(dcPingTraffic()) << "IP header: Lenght" << ipHeaderLength
<< "Sender:" << senderAddress.toString()
<< "Destination:" << destinationAddress.toString()
<< "Size:" << htons(ipHeader->tot_len) << "B"
<< "TTL" << ipHeader->ttl;
struct icmp *responsePacket = reinterpret_cast<struct icmp *>(receiveBuffer + ipHeaderLength);
qCDebug(dcPingTraffic()) << "ICMP packt (Size:" << icmpPacketSize << "Bytes):"
<< "Type" << responsePacket->icmp_type
<< "Code:" << responsePacket->icmp_code
<< "ID:" << QString("0x%1").arg(responsePacket->icmp_id, 4, 16, QChar('0'))
<< "Sequence:" << responsePacket->icmp_seq;
if (responsePacket->icmp_type == ICMP_ECHOREPLY) {
PingReply *reply = m_pendingReplies.take(responsePacket->icmp_id);
if (!reply) {
qCDebug(dcPing()) << "No pending reply for ping echo response with id" << QString("0x%1").arg(responsePacket->icmp_id, 4, 16, QChar('0')) << "Sequence:" << htons(responsePacket->icmp_seq) << "from" << senderAddress.toString();
return;
}
// Make sure the sender matches the target
if (reply->targetHostAddress() != senderAddress) {
qCWarning(dcPing()) << "Received id for different target reply" << reply->targetHostAddress().toString() << "!=" << senderAddress.toString();
finishReply(reply, PingReply::ErrorHostUnreachable);
return;
}
// Verify sequence number
if (responsePacket->icmp_seq != reply->sequenceNumber()) {
qCWarning(dcPing()) << "Received echo reply with different sequence number" << htons(responsePacket->icmp_seq);
finishReply(reply, PingReply::ErrorInvalidResponse);
return;
}
// Calculate ping duration 2 digits accuracy
struct timeval receiveTimeValue;
gettimeofday(&receiveTimeValue, nullptr);
timeValueSubtract(&receiveTimeValue, &reply->m_startTime);
reply->m_duration = qRound((receiveTimeValue.tv_sec * 1000 + (double)receiveTimeValue.tv_usec / 1000) * 100) / 100.0;
qCDebug(dcPing()) << "Received ICMP response" << reply->targetHostAddress().toString() << ICMP_PACKET_SIZE << "[Bytes]"
<< "ID:" << QString("0x%1").arg(responsePacket->icmp_id, 4, 16, QChar('0'))
<< "Sequence:" << htons(responsePacket->icmp_seq)
<< "Time:" << reply->duration() << "[ms]";
if (reply->doHostLookup()) {
// Note: due to a Qt bug < 5.9 we need to use old SLOT style and cannot make use of lambda here
int lookupId = QHostInfo::lookupHost(senderAddress.toString(), this, SLOT(onHostLookupFinished(QHostInfo)));
m_pendingHostLookups.insert(lookupId, reply);
} else {
finishReply(reply, PingReply::ErrorNoError);
}
} else if (responsePacket->icmp_type == ICMP_DEST_UNREACH) {
// Get the sending package
int messageOffset = sizeof(struct iphdr) + 8;
struct iphdr *nestedIpHeader = (struct iphdr *)(receiveBuffer + messageOffset);
int nestedIpHeaderLength = nestedIpHeader->ihl << 2;
int nestedIcmpPacketSize = htons(nestedIpHeader->tot_len) - nestedIpHeaderLength;
QHostAddress nestedSenderAddress(qFromBigEndian(nestedIpHeader->saddr));
QHostAddress nestedDestinationAddress(qFromBigEndian(nestedIpHeader->daddr));
qCDebug(dcPingTraffic()) << "++ IP header: Lenght" << nestedIpHeaderLength
<< "Sender:" << nestedSenderAddress.toString()
<< "Destination:" << nestedDestinationAddress.toString()
<< "Size:" << htons(nestedIpHeader->tot_len) << "B"
<< "TTL" << ipHeader->ttl;
struct icmp *nestedResponsePacket = reinterpret_cast<struct icmp *>(receiveBuffer + messageOffset + nestedIpHeaderLength);
qCDebug(dcPingTraffic()) << "++ ICMP packt (Size:" << nestedIcmpPacketSize << "Bytes):"
<< "Type" << nestedResponsePacket->icmp_type
<< "Code:" << nestedResponsePacket->icmp_code
<< "ID:" << QString("0x%1").arg(nestedResponsePacket->icmp_id, 4, 16, QChar('0'))
<< "Sequence:" << nestedResponsePacket->icmp_seq;
qCDebug(dcPing()) << "ICMP destination unreachable" << nestedDestinationAddress.toString()
<< "Code:" << nestedResponsePacket->icmp_code
<< "ID:" << QString("0x%1").arg(nestedResponsePacket->icmp_id, 4, 16, QChar('0'))
<< "Sequence:" << htons(nestedResponsePacket->icmp_seq);
PingReply *reply = m_pendingReplies.take(nestedResponsePacket->icmp_id);
if (!reply) {
qCDebug(dcPingTraffic()) << "No pending reply for ping echo response unreachable with ID"
<< QString("0x%1").arg(nestedResponsePacket->icmp_id, 4, 16, QChar('0'))
<< "Sequence:" << htons(nestedResponsePacket->icmp_seq)
<< "from" << nestedSenderAddress.toString() << "to" << nestedDestinationAddress.toString();
return;
}
finishReply(reply, PingReply::ErrorHostUnreachable);
}
}
}
void Ping::onHostLookupFinished(const QHostInfo &info)
{
PingReply *reply = m_pendingHostLookups.value(info.lookupId());
if (!reply) {
qCWarning(dcPing()) << "Could not find reply after host lookup.";
return;
}
if (info.error() != QHostInfo::NoError) {
qCWarning(dcPing()) << "Failed to look up hostname after successfull ping" << reply->targetHostAddress().toString() << info.error();
} else {
qCDebug(dcPing()) << "Looked up hostname after successfull ping" << reply->targetHostAddress().toString() << info.hostName();
if (info.hostName() != reply->targetHostAddress().toString()) {
reply->m_hostName = info.hostName();
}
}
finishReply(reply, PingReply::ErrorNoError);
}