/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 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 . * * 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 #include #include #include #include #include #include #include #include #include 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(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) { reply->m_requestId = calculateRequestId(); } requestPacket.icmpHeadr.un.echo.id = htons(reply->requestId()); requestPacket.icmpHeadr.un.echo.sequence = htons(reply->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(&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); } qCDebug(dcPingTraffic()) << "Send ICMP echo request" << reply->targetHostAddress().toString() << ICMP_PACKET_SIZE << "[Bytes]" << "ID:" << QString("0x%1").arg(reply->requestId(), 4, 16, QChar('0')) << "Sequence:" << reply->sequenceNumber(); // 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); }); connect(reply, &PingReply::finished, this, [=](){ reply->deleteLater(); // Cleanup any retry left over queue stuff m_pendingReplies.remove(reply->requestId()); m_replyQueue.removeAll(reply); }); 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(); } else { // Note: don't remove from m_pendingReplies to prevent // double assignmet of request id's between 2 retries reply->m_error = error; reply->m_retryCount++; reply->m_sequenceNumber++; if (reply->m_retries > 1) { qCDebug(dcPing()) << "Ping finished with error" << error << "Retry ping" << reply->targetHostAddress().toString() << reply->m_retryCount << "/" << reply->m_retries; } else { qCDebug(dcPing()) << "Ping finished with error" << error << "Retry ping" << reply->targetHostAddress().toString(); } 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(receiveBuffer + ipHeaderLength); quint16 icmpId = htons(responsePacket->icmp_id); quint16 icmpSequnceNumber = htons(responsePacket->icmp_seq); qCDebug(dcPingTraffic()) << "ICMP packt (Size:" << icmpPacketSize << "Bytes):" << "Type" << responsePacket->icmp_type << "Code:" << responsePacket->icmp_code << "ID:" << QString("0x%1").arg(icmpId, 4, 16, QChar('0')) << "Sequence:" << icmpSequnceNumber; if (responsePacket->icmp_type == ICMP_ECHOREPLY) { PingReply *reply = m_pendingReplies.take(icmpId); if (!reply) { qCDebug(dcPing()) << "No pending reply for ping echo response with id" << QString("0x%1").arg(icmpId, 4, 16, QChar('0')) << "Sequence:" << icmpSequnceNumber << "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 (icmpSequnceNumber != reply->sequenceNumber()) { qCWarning(dcPing()) << "Received echo reply with different sequence number" << icmpSequnceNumber; 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(icmpId, 4, 16, QChar('0')) << "Sequence:" << icmpSequnceNumber << "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(receiveBuffer + messageOffset + nestedIpHeaderLength); icmpId = htons(nestedResponsePacket->icmp_id); icmpSequnceNumber = htons(nestedResponsePacket->icmp_seq); qCDebug(dcPingTraffic()) << "++ ICMP packt (Size:" << nestedIcmpPacketSize << "Bytes):" << "Type" << nestedResponsePacket->icmp_type << "Code:" << nestedResponsePacket->icmp_code << "ID:" << QString("0x%1").arg(icmpId, 4, 16, QChar('0')) << "Sequence:" << icmpSequnceNumber; qCDebug(dcPing()) << "ICMP destination unreachable" << nestedDestinationAddress.toString() << "Code:" << nestedResponsePacket->icmp_code << "ID:" << QString("0x%1").arg(icmpId, 4, 16, QChar('0')) << "Sequence:" << icmpSequnceNumber; PingReply *reply = m_pendingReplies.take(icmpId); if (!reply) { qCDebug(dcPingTraffic()) << "No pending reply for ping echo response unreachable with ID" << QString("0x%1").arg(icmpId, 4, 16, QChar('0')) << "Sequence:" << icmpSequnceNumber << "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); }