diff --git a/.gitignore b/.gitignore index d2c08e73..e404ce66 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ snap/prime/ snap/stage/ doc/interfacelist.qdoc +# Never add downloaded json data +data/mac-database/macaddress.io-db.json + .crossbuilder/ debs*.tar diff --git a/README.md b/README.md index ff4ea151..1ca5ddf4 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,15 @@ Chat with us on [Telegram](http://t.me/nymeacommunity) or [Discord](https://disc A detailed documentation on how to develop with *nymea* is available on the [nymea | developer documentation](https://nymea.io/documentation/developers/). +## Network discovery + +When starting nymead as user without root privileges, the network device discovery will not available due to missing raw socket permission. +If you still want to make use of this feature, the binary capabilities need to be adjusted. + + sudo setcap cap_net_admin,cap_net_raw=eip /usr/bin/nymead + +This will allow nymead to create raw sockets for ARP and ICMP network discovery tools even when nymead gets started as user without root privileges. + ## License -------------------------------------------- > nymea is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3 of the License. diff --git a/data/mac-database/README.md b/data/mac-database/README.md new file mode 100644 index 00000000..dc2f77bd --- /dev/null +++ b/data/mac-database/README.md @@ -0,0 +1,21 @@ +# Building the MAC address database + +The MAC address database can be created using the `build-database.py` script. +The script will download the latest registered MAC address block information +from [https://macaddress.io](https://macaddress.io) and creates a size and access optimized +SQLITE database file. + +The generated database is read performance optimized and tried to keep as small as possible for +searching MAC address OUIs (Organizationally Unique Identifiers) blocks and returning the registered company name. + + $ python3 build-database.py + +The final database will be named `mac-addresses.db`. + +In nymea the `MacAddressDatabase` class will provide access to this generated database and provides an asynch threaded mechanism to +get the company name for a given MAC address. + +The database will be searched in the system default data location `${XDG_DATA_DIRS}/nymead/`. + +On debian package based system the database file will be installed in `/usr/share/nymea/nymead/mac-addresses.db`. + diff --git a/data/mac-database/build-database.py b/data/mac-database/build-database.py new file mode 100644 index 00000000..193457de --- /dev/null +++ b/data/mac-database/build-database.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python + +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# +# 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 +# +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + +import os +import sys +import json +import sqlite3 +import requests + +downloadUrl='https://macaddress.io/database/macaddress.io-db.json' +jsonDataFileName = 'macaddress.io-db.json' +databaseFileName = 'mac-addresses.db' + +print('Downloading', downloadUrl, '...') +downloadRequest = requests.get(downloadUrl) +open(jsonDataFileName, 'wb').write(downloadRequest.content) + +print('Reading JSON data..') +vendorInfoHash = {} +jsonDataFile = open(jsonDataFileName, 'r') +lines = jsonDataFile.readlines() +for line in lines: + vendorMap = json.loads(line) + vendorInfoHash[vendorMap['oui']] = vendorMap['companyName'] + +jsonDataFile.close() + +if os.path.exists(databaseFileName): + print('Delete old db file', databaseFileName) + os.remove(databaseFileName) + +print('Build up database', databaseFileName) +connection = sqlite3.connect(databaseFileName) +cursor = connection.cursor() +cursor.execute('CREATE TABLE companyNames (companyName TEXT PRIMARY KEY, UNIQUE(companyName));') +cursor.execute('CREATE TABLE oui (oui TEXT PRIMARY KEY, companyNameIndex INTEGER, UNIQUE(oui)) WITHOUT ROWID;') +#cursor.execute('CREATE UNIQUE INDEX ouiIndex ON `oui` (`oui`);') + +# Insert all vendor names alphabetically +print('Writing company names into database...') +sortedVendorHash = sorted(vendorInfoHash.items(), key=lambda x: x[1], reverse=False) +vendorCount = 0 +for vendorInfo in sortedVendorHash: + insertQuery = 'INSERT OR IGNORE INTO companyNames (companyName) VALUES(?);' + cursor.execute(insertQuery, [vendorInfo[1]]) + cursor.execute('SELECT COUNT(companyName) FROM companyNames'); + countResult = cursor.fetchall() + vendorCount = countResult[0][0] + +connection.commit() + +# Insert all oui with reference to company name +print('Writing OUI into database with company name refference...') +# Sort by oui for good binary search in the db +sortedOuiHash = sorted(vendorInfoHash.items(), key=lambda x: x[0], reverse=False) +ouiCount = 0 +for vendorInfo in sortedOuiHash: + insertQuery = 'INSERT OR IGNORE INTO oui (oui, companyNameIndex) VALUES(?, (SELECT rowid FROM companyNames WHERE companyName = ?));' + cursor.execute(insertQuery, [vendorInfo[0].replace(':', ''), vendorInfo[1]]) + cursor.execute('SELECT COUNT(oui) FROM oui'); + countResult = cursor.fetchall() + ouiCount = countResult[0][0] + +connection.commit() +connection.close() +print('Finished successfully. Loaded', ouiCount, 'OUI values from', vendorCount, 'manufacturers into', databaseFileName) + + diff --git a/data/mac-database/mac-addresses.db b/data/mac-database/mac-addresses.db new file mode 100644 index 00000000..9494d819 Binary files /dev/null and b/data/mac-database/mac-addresses.db differ diff --git a/debian/control b/debian/control index 62258bd4..f87afa28 100644 --- a/debian/control +++ b/debian/control @@ -59,6 +59,7 @@ Depends: libqt5network5, libqt5websockets5, libqt5bluetooth5, libqt5sql5-sqlite, + libcap2-bin, logrotate, bluez, tar, @@ -70,7 +71,8 @@ Depends: libqt5network5, libnymea1 (= ${binary:Version}), ${shlibs:Depends}, ${misc:Depends} -Recommends: nymea-cli, +Recommends: nymea-cli, + nymea-data, network-manager, nymea-update-plugin-impl, nymea-system-plugin-impl, @@ -86,6 +88,22 @@ Description: An open source IoT server - daemon This package will install the daemon. +Package: nymea-data +Section: misc +Architecture: all +Depends: ${misc:Depends} +Recommends: nymea +Description: Optional data for extending functionality in nymea daemon + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package provides the MAC address database for nymead and for the plugins in + order to get the manufacturer name for a given MAC address. + + Package: nymea-doc Section: doc Architecture: all diff --git a/debian/nymea-data.install b/debian/nymea-data.install new file mode 100644 index 00000000..df0d954c --- /dev/null +++ b/debian/nymea-data.install @@ -0,0 +1,2 @@ +data/mac-database/mac-addresses.db usr/share/nymea/nymead/ + diff --git a/debian/nymead.postinst b/debian/nymead.postinst index 0c5f1b3e..faf9f178 100755 --- a/debian/nymead.postinst +++ b/debian/nymead.postinst @@ -2,7 +2,7 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # -# Copyright (C) 2015-2016 Simon Stuerz # +# Copyright (C) 2015 - 2021 nymea GmbH # # # # This file is part of nymea. # # # @@ -20,6 +20,15 @@ # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# Make sure user will be able to perform a networkdiscovery +# using ARP and ICMP sockets (raw_sock). +setcap cap_net_admin,cap_net_raw=eip /usr/bin/nymead +if [ $? -eq 0 ]; then + echo "Set raw socket network capabilities successfully for nymead." +else + echo "Failed to set raw socket network capabilities for nymead. Network device discovery will not be available for non root users." +fi + # Restart the nymea daemon after update if it's running systemctl daemon-reload systemctl status nymead > /dev/null 2>&1 diff --git a/libnymea-core/hardwaremanagerimplementation.cpp b/libnymea-core/hardwaremanagerimplementation.cpp index e873b839..87d52b68 100644 --- a/libnymea-core/hardwaremanagerimplementation.cpp +++ b/libnymea-core/hardwaremanagerimplementation.cpp @@ -46,6 +46,7 @@ #include "hardware/modbus/modbusrtumanager.h" #include "hardware/modbus/modbusrtuhardwareresourceimplementation.h" +#include "network/networkdevicediscovery.h" namespace nymeaserver { @@ -78,6 +79,8 @@ HardwareManagerImplementation::HardwareManagerImplementation(Platform *platform, m_modbusRtuResource = new ModbusRtuHardwareResourceImplementation(modbusRtuManager, this); + m_networkDeviceDiscovery = new NetworkDeviceDiscovery(this); + // Enable all the resources setResourceEnabled(m_pluginTimerManager, true); setResourceEnabled(m_radio433, true); @@ -100,6 +103,7 @@ HardwareManagerImplementation::HardwareManagerImplementation(Platform *platform, HardwareManagerImplementation::~HardwareManagerImplementation() { + } Radio433 *HardwareManagerImplementation::radio433() @@ -152,6 +156,11 @@ ModbusRtuHardwareResource *HardwareManagerImplementation::modbusRtuResource() return m_modbusRtuResource; } +NetworkDeviceDiscovery *HardwareManagerImplementation::networkDeviceDiscovery() +{ + return m_networkDeviceDiscovery; +} + void HardwareManagerImplementation::thingsLoaded() { m_zigbeeResource->thingsLoaded(); diff --git a/libnymea-core/hardwaremanagerimplementation.h b/libnymea-core/hardwaremanagerimplementation.h index c808b0d2..c94153ef 100644 --- a/libnymea-core/hardwaremanagerimplementation.h +++ b/libnymea-core/hardwaremanagerimplementation.h @@ -64,6 +64,7 @@ public: I2CManager *i2cManager() override; ZigbeeHardwareResource *zigbeeResource() override; ModbusRtuHardwareResource *modbusRtuResource() override; + NetworkDeviceDiscovery *networkDeviceDiscovery() override; public slots: void thingsLoaded(); @@ -83,6 +84,8 @@ private: I2CManager *m_i2cManager = nullptr; ZigbeeHardwareResourceImplementation *m_zigbeeResource = nullptr; ModbusRtuHardwareResourceImplementation *m_modbusRtuResource = nullptr; + NetworkDeviceDiscovery *m_networkDeviceDiscovery = nullptr; + }; } diff --git a/libnymea/hardwaremanager.h b/libnymea/hardwaremanager.h index 4dab713e..893fa340 100644 --- a/libnymea/hardwaremanager.h +++ b/libnymea/hardwaremanager.h @@ -45,6 +45,7 @@ class I2CManager; class ZigbeeHardwareResource; class HardwareResource; class ModbusRtuHardwareResource; +class NetworkDeviceDiscovery; class HardwareManager : public QObject { @@ -65,6 +66,7 @@ public: virtual I2CManager *i2cManager() = 0; virtual ZigbeeHardwareResource *zigbeeResource() = 0; virtual ModbusRtuHardwareResource *modbusRtuResource() = 0; + virtual NetworkDeviceDiscovery *networkDeviceDiscovery() = 0; protected: void setResourceEnabled(HardwareResource* resource, bool enabled); diff --git a/libnymea/libnymea.pro b/libnymea/libnymea.pro index 41752d75..26d05539 100644 --- a/libnymea/libnymea.pro +++ b/libnymea/libnymea.pro @@ -3,8 +3,9 @@ include(../nymea.pri) TARGET = nymea TEMPLATE = lib -QT += network bluetooth dbus serialport +QT += network bluetooth dbus serialport sql QT -= gui + DEFINES += LIBNYMEA_LIBRARY CONFIG += link_pkgconfig @@ -42,6 +43,15 @@ HEADERS += \ network/apikeys/apikey.h \ network/apikeys/apikeysprovider.h \ network/apikeys/apikeystorage.h \ + network/arpsocket.h \ + network/macaddressdatabase.h \ + network/networkdevicediscovery.h \ + network/networkdevicediscoveryreply.h \ + network/networkdeviceinfo.h \ + network/networkdeviceinfos.h \ + network/networkutils.h \ + network/ping.h \ + network/pingreply.h \ platform/package.h \ platform/repository.h \ types/browseritem.h \ @@ -140,6 +150,15 @@ SOURCES += \ network/apikeys/apikey.cpp \ network/apikeys/apikeysprovider.cpp \ network/apikeys/apikeystorage.cpp \ + network/arpsocket.cpp \ + network/macaddressdatabase.cpp \ + network/networkdevicediscovery.cpp \ + network/networkdevicediscoveryreply.cpp \ + network/networkdeviceinfo.cpp \ + network/networkdeviceinfos.cpp \ + network/networkutils.cpp \ + network/ping.cpp \ + network/pingreply.cpp \ nymeasettings.cpp \ platform/package.cpp \ platform/repository.cpp \ diff --git a/libnymea/network/arpsocket.cpp b/libnymea/network/arpsocket.cpp new file mode 100644 index 00000000..a05e84af --- /dev/null +++ b/libnymea/network/arpsocket.cpp @@ -0,0 +1,467 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "arpsocket.h" +#include "loggingcategories.h" +#include "networkutils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +NYMEA_LOGGING_CATEGORY(dcArpSocket, "ArpSocket") +NYMEA_LOGGING_CATEGORY(dcArpSocketTraffic, "ArpSocketTraffic") + +#define ETHER_PROTOCOL_LEN 4 // Length of the IPv4 address +#define ETHER_HEADER_LEN sizeof(struct ether_header) +#define ETHER_ARP_LEN sizeof(struct ether_arp) +#define ETHER_ARP_PACKET_LEN ETHER_HEADER_LEN + ETHER_ARP_LEN + +ArpSocket::ArpSocket(QObject *parent) : QObject(parent) +{ + +} + +bool ArpSocket::sendRequest() +{ + if (!m_isOpen) + return false; + + // Send the ARP request trough each network interface + qCDebug(dcArpSocket()) << "Sending ARP request to all local network interfaces..."; + foreach (const QNetworkInterface &networkInterface, QNetworkInterface::allInterfaces()) { + sendRequest(networkInterface); + } + + return true; +} + +bool ArpSocket::sendRequest(const QString &interfaceName) +{ + if (!m_isOpen) + return false; + + + // Get the interface + qCDebug(dcArpSocket()) << "Sending ARP request to all network interfaces" << interfaceName << "..."; + QNetworkInterface networkInterface = QNetworkInterface::interfaceFromName(interfaceName); + if (!networkInterface.isValid()) { + qCWarning(dcArpSocket()) << "Failed to send the ARP request to network interface" << interfaceName << "because the interface is not valid."; + return false; + } + + loadArpCache(networkInterface); + return sendRequest(networkInterface); +} + +bool ArpSocket::sendRequest(const QNetworkInterface &networkInterface) +{ + if (!m_isOpen) + return false; + + // Skip local host + if (networkInterface.flags().testFlag(QNetworkInterface::IsLoopBack)) + return false; + + // If have no interface indes, we cannot use this network + if (networkInterface.index() == 0) { + qCDebug(dcArpSocket()) << "Failed to send the ARP request to network interface" << networkInterface.name() << "because the system interface index is unknown."; + return false; + } + + // Check if the interface is up and running + if (!networkInterface.flags().testFlag(QNetworkInterface::IsUp)) { + qCDebug(dcArpSocket()) << "Failed to send the ARP request to network interface" << networkInterface.name() << "because it is not up."; + return false; + } + + if (!networkInterface.flags().testFlag(QNetworkInterface::IsRunning)) { + qCDebug(dcArpSocket()) << "Failed to send the ARP request to network interface" << networkInterface.name() << "because it is not running."; + return false; + } + + // Verify we have a hardware address (virtual network interfaces like tunnels) + if (networkInterface.hardwareAddress().isEmpty()) { + qCDebug(dcArpSocket()) << "Failed to send the ARP request to network interface" << networkInterface.name() << "because there is no hardware address which is required for ARP."; + return false; + } + + loadArpCache(networkInterface); + + qCDebug(dcArpSocket()) << "Verifying network interface" << networkInterface.name() << networkInterface.hardwareAddress() << "..."; + foreach (const QNetworkAddressEntry &entry, networkInterface.addressEntries()) { + // Only IPv4 + if (entry.ip().protocol() != QAbstractSocket::IPv4Protocol) + continue; + + qCDebug(dcArpSocket()) << " Host address:" << entry.ip().toString(); + qCDebug(dcArpSocket()) << " Broadcast address:" << entry.broadcast().toString(); + qCDebug(dcArpSocket()) << " Netmask:" << entry.netmask().toString(); + quint32 addressRangeStart = entry.ip().toIPv4Address() & entry.netmask().toIPv4Address(); + quint32 addressRangeStop = entry.broadcast().toIPv4Address() | addressRangeStart; + quint32 range = addressRangeStop - addressRangeStart; + qCDebug(dcArpSocket()) << " Address range" << range << " | from" << QHostAddress(addressRangeStart).toString() << "-->" << QHostAddress(addressRangeStop).toString(); + if (range > 255) { + qCWarning(dcArpSocket()) << "Not sending ARP requests to the network" << networkInterface.name() << "because it has a to wide range for ARP broadcast pinging."; + return false; + } + + qCDebug(dcArpSocket()) << "Start sending ARP requests to each host within the range..."; + + // Send ARP request to each address within the range + for (quint32 i = 0; i < range; i++) { + quint32 address = addressRangeStart + i; + QHostAddress targetAddress(address); + if (targetAddress == entry.ip()) + continue; + + sendRequestInternally(networkInterface.index(), networkInterface.hardwareAddress(), entry.ip(), "ff:ff:ff:ff:ff:ff", targetAddress); + } + } + + return true; +} + +bool ArpSocket::sendRequest(const QHostAddress &targetAddress) +{ + if (!m_isOpen) + return false; + + if (targetAddress.protocol() != QAbstractSocket::IPv4Protocol) { + qCWarning(dcArpSocket()) << "Not sending ARP request to host" << targetAddress << "because only IPv4 is supported."; + return false; + } + + qCDebug(dcArpSocket()) << "Sending ARP request to host" << targetAddress.toString() << "..."; + foreach (const QNetworkInterface &networkInterface, QNetworkInterface::allInterfaces()) { + foreach (const QNetworkAddressEntry &entry, networkInterface.addressEntries()) { + // Only IPv4 + if (entry.ip().protocol() != QAbstractSocket::IPv4Protocol) + continue; + + if (targetAddress.isInSubnet(entry.ip(), entry.netmask().toIPv4Address())) { + return sendRequestInternally(networkInterface.index(), networkInterface.hardwareAddress(), entry.ip(), "ff:ff:ff:ff:ff:ff", targetAddress); + } + } + } + + qCWarning(dcArpSocket()) << "Failed to send ARP request to" << targetAddress.toString() << "because no valid network interface could be found."; + return false; +} + +bool ArpSocket::isOpen() const +{ + return m_isOpen; +} + +bool ArpSocket::openSocket() +{ + qCDebug(dcArpSocket()) << "Open ARP socket..."; + + if (m_isOpen) { + qCWarning(dcArpSocket()) << "Failed to enable ARP scanner because the scanner is already running."; + return false; + } + + // Build socket descriptor + m_socketDescriptor = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ARP)); + if (m_socketDescriptor < 0) { + qCWarning(dcArpSocket()) << "Failed to create the ARP capture socket for" << "." << strerror(errno); + return false; + } + + // Configure non blocking + if (fcntl(m_socketDescriptor, F_SETFL, fcntl(m_socketDescriptor, F_GETFL, 0) | O_NONBLOCK) != 0) { + qCWarning(dcArpSocket()) << "Failed to set the ARP socket function control to non-blocking" << strerror(errno); + close(m_socketDescriptor); + return false; + } + + m_socketNotifier = new QSocketNotifier(m_socketDescriptor, QSocketNotifier::Read, this); + m_socketNotifier->setEnabled(false); + connect(m_socketNotifier, &QSocketNotifier::activated, this, [=](int socket){ + if (socket != m_socketDescriptor) + return; + + // Make sure to read all data from the socket... + while (true) { + char receiveBuffer[ETHER_ARP_PACKET_LEN]; + memset(&receiveBuffer, 0, sizeof(receiveBuffer)); + + // Read the buffer + int bytesReceived = recv(m_socketDescriptor, receiveBuffer, ETHER_ARP_PACKET_LEN, 0); + if (bytesReceived < 0) { + // Finished reading + return; + } + + // Parse data using structs header + arp + struct ether_header *etherHeader = (struct ether_header *)(receiveBuffer); + struct ether_arp *arpPacket = (struct ether_arp *)(receiveBuffer + ETHER_HEADER_LEN); + QString senderMacAddress = getMacAddressString(arpPacket->arp_sha); + QHostAddress senderHostAddress = getHostAddressString(arpPacket->arp_spa); + QString targetMacAddress = getMacAddressString(arpPacket->arp_tha); + QHostAddress targetHostAddress = getHostAddressString(arpPacket->arp_tpa); + uint16_t etherType = htons(etherHeader->ether_type); + if (etherType != ETHERTYPE_ARP) { + qCWarning(dcArpSocketTraffic()) << "Received ARP socket data header with invalid type" << etherType; + return; + } + + // Filter for ARP replies + uint16_t arpOperationCode = htons(arpPacket->arp_op); + switch (arpOperationCode) { + case ARPOP_REQUEST: + //qCDebug(dcArpSocket()) << "ARP request from " << senderMacAddress << senderHostAddress.toString() << "-->" << targetMacAddress << targetHostAddress.toString(); + break; + case ARPOP_REPLY: { + QNetworkInterface networkInterface = NetworkUtils::getInterfaceForMacAddress(targetMacAddress); + if (!networkInterface.isValid()) { + qCWarning(dcArpSocket()) << "Could not find interface from ARP response" << targetHostAddress.toString() << targetMacAddress; + return; + } + + qCDebug(dcArpSocketTraffic()) << "ARP response from" << senderMacAddress << senderHostAddress.toString() << "on" << networkInterface.name(); + emit arpResponse(networkInterface, senderHostAddress, senderMacAddress.toLower()); + break; + } + case ARPOP_RREQUEST: + qCDebug(dcArpSocketTraffic()) << "RARP request from" << senderMacAddress << senderHostAddress.toString() << "-->" << targetMacAddress << targetHostAddress.toString(); + break; + case ARPOP_RREPLY: + qCDebug(dcArpSocketTraffic()) << "PARP response from" << senderMacAddress << senderHostAddress.toString() << "-->" << targetMacAddress << targetHostAddress.toString(); + break; + case ARPOP_InREQUEST: + qCDebug(dcArpSocketTraffic()) << "InARP request from" << senderMacAddress << senderHostAddress.toString() << "-->" << targetMacAddress << targetHostAddress.toString(); + break; + case ARPOP_InREPLY: + qCDebug(dcArpSocketTraffic()) << "InARP response from" << senderMacAddress << senderHostAddress.toString() << "-->" << targetMacAddress << targetHostAddress.toString(); + break; + case ARPOP_NAK: + qCDebug(dcArpSocketTraffic()) << "(ATM)ARP NAK from" << senderMacAddress << senderHostAddress.toString() << "-->" << targetMacAddress << targetHostAddress.toString(); + break; + default: + qCWarning(dcArpSocketTraffic()) << "Received unhandled ARP operation code" << arpOperationCode << "from" << senderMacAddress << senderHostAddress.toString(); + break; + } + } + }); + + m_socketNotifier->setEnabled(true); + m_isOpen = true; + qCDebug(dcArpSocket()) << "ARP enabled successfully"; + + // Send broadcast request + //sendRequest(); + return true; +} + +void ArpSocket::closeSocket() +{ + m_isOpen = false; + + if (m_socketNotifier) { + m_socketNotifier->setEnabled(false); + delete m_socketNotifier; + m_socketNotifier = nullptr; + } + + if (m_socketDescriptor >= 0) { + close(m_socketDescriptor); + m_socketDescriptor = -1; + } + + qCDebug(dcArpSocket()) << "ARP disabled successfully"; +} + +bool ArpSocket::sendRequestInternally(int networkInterfaceIndex, const QString &senderMacAddress, const QHostAddress &senderHostAddress, const QString &targetMacAddress, const QHostAddress &targetHostAddress) +{ + // Set up data structures + unsigned char sendingBuffer[ETHER_ARP_PACKET_LEN]; + memset(sendingBuffer, 0, ETHER_ARP_PACKET_LEN); + struct ether_header *etherHeader = (struct ether_header *)sendingBuffer; + struct ether_arp *arpPacket = (struct ether_arp *)(sendingBuffer + sizeof(struct ether_header)); + + // Build the ethernet header + fillMacAddress(etherHeader->ether_dhost, targetMacAddress); + fillMacAddress(etherHeader->ether_shost, senderMacAddress); + etherHeader->ether_type = htons(ETHERTYPE_ARP); + + // Build the ARP header + arpPacket->ea_hdr.ar_hrd = htons(ARPHRD_ETHER); + arpPacket->ea_hdr.ar_pro = htons(ETH_P_IP); + arpPacket->ea_hdr.ar_hln = ETHER_ADDR_LEN; + arpPacket->ea_hdr.ar_pln = ETHER_PROTOCOL_LEN; + arpPacket->ea_hdr.ar_op = htons(ARPOP_REQUEST); + + // Write the ARP packet + fillMacAddress(arpPacket->arp_sha, senderMacAddress); + fillHostAddress(arpPacket->arp_spa, senderHostAddress); + fillMacAddress(arpPacket->arp_tha, targetMacAddress); + fillHostAddress(arpPacket->arp_tpa, targetHostAddress); + + struct sockaddr_ll socketAddress; + memset(&socketAddress, 0, sizeof(socketAddress)); + socketAddress.sll_family = AF_PACKET; + socketAddress.sll_protocol = htons(ETH_P_ARP); + socketAddress.sll_ifindex = networkInterfaceIndex; + socketAddress.sll_hatype = htons(ARPHRD_ETHER); + socketAddress.sll_pkttype = PACKET_BROADCAST; + socketAddress.sll_halen = ETH_ALEN; + memset(socketAddress.sll_addr, 0x00, 6); + + //qCDebug(dcArpSocket()) << "Send ARP request to" << targetHostAddress.toString(); + int bytesSent = sendto(m_socketDescriptor, sendingBuffer, ETHER_ARP_PACKET_LEN, 0, (struct sockaddr *)&socketAddress, sizeof(socketAddress)); + if (bytesSent < 0) { + qCWarning(dcArpSocket()) << "Failed to send ARP packet data to" << targetHostAddress.toString() << strerror(errno); + return false; + } + + return true; +} + +QString ArpSocket::getMacAddressString(uint8_t *senderHardwareAddress) +{ + QStringList hexValues; + for (int i = 0; i < ETHER_ADDR_LEN; i++) { + hexValues.append(QString("%1").arg(senderHardwareAddress[i], 2, 16, QLatin1Char('0'))); + } + + return hexValues.join(":"); +} + +QHostAddress ArpSocket::getHostAddressString(uint8_t *senderIpAddress) +{ + QStringList values; + for (int i = 0; i < ETHER_PROTOCOL_LEN; i++) { + values.append(QString("%1").arg(senderIpAddress[i])); + } + + return QHostAddress(values.join(".")); +} + +bool ArpSocket::loadArpCache(const QNetworkInterface &interface) +{ + QFile arpFile("/proc/net/arp"); + qCDebug(dcArpSocket()) << "Loading ARP cache from system" << arpFile.fileName() << "..."; + if (!arpFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCWarning(dcArpSocket()) << "Failed to load ARP cache from" << arpFile.fileName() << arpFile.errorString(); + return false; + } + + // Read all data + QByteArray data = arpFile.readAll(); + arpFile.close(); + + // Parse data line by line + int lineCount = -1; + QTextStream stream(&data); + while (!stream.atEnd()) { + QString line = stream.readLine(); + lineCount += 1; + // Skip the first line since it's just the header + if (lineCount == 0) + continue; + + + //qCDebug(dcArpSocket()) << "Checking line" << line; + + QStringList columns = line.split(QLatin1Char(' ')); + columns.removeAll(""); + + // Make sure we have enought token + if (columns.count() < 6) { + qCWarning(dcArpSocket()) << "Line has invalid column count" << line; + continue; + } + + QHostAddress address(columns.at(0).trimmed()); + if (address.isNull()) { + qCWarning(dcArpSocket()) << "Line has invalid address"; + continue; + } + + QString macAddress = columns.at(3).trimmed(); + if (macAddress.count() != 17) { + qCWarning(dcArpSocket()) << "Line has invalid mac address" << columns << macAddress; + continue; + } + + QNetworkInterface addressInterface = QNetworkInterface::interfaceFromName(columns.at(5)); + if (!addressInterface.isValid()) + continue; + + // Check if we filter for specific interfaces + if (interface.isValid() && addressInterface.name() != interface.name()) + continue; + + qCDebug(dcArpSocket()) << "Loaded from cache" << address.toString() << macAddress << addressInterface.name(); + emit arpResponse(addressInterface, address, macAddress); + } + + return true; +} + +void ArpSocket::fillMacAddress(uint8_t *targetArray, const QString &macAddress) +{ + QStringList macValues = macAddress.split(":"); + for (int i = 0; i < ETHER_ADDR_LEN; i++) { + targetArray[i] = macValues.at(i).toUInt(nullptr, 16); + } +} + +void ArpSocket::fillHostAddress(uint8_t *targetArray, const QHostAddress &hostAddress) +{ + QByteArray hostData; + QDataStream stream(&hostData, QIODevice::WriteOnly); + stream.setByteOrder(QDataStream::BigEndian); + stream << hostAddress.toIPv4Address(); + for (int i = 0; i < ETHER_PROTOCOL_LEN; i++) { + targetArray[i] = hostData.at(i); + } +} + diff --git a/libnymea/network/arpsocket.h b/libnymea/network/arpsocket.h new file mode 100644 index 00000000..2bc58175 --- /dev/null +++ b/libnymea/network/arpsocket.h @@ -0,0 +1,86 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef ARPSOCKET_H +#define ARPSOCKET_H + +#include +#include +#include +#include +#include +#include + +#include "libnymea.h" + +class LIBNYMEA_EXPORT ArpSocket : public QObject +{ + Q_OBJECT +public: + explicit ArpSocket(QObject *parent = nullptr); + + // Send ARP request to all local networks + bool sendRequest(); + + // Send ARP request to a specific network interface with the given name + bool sendRequest(const QString &interfaceName); + + // Send ARP request to a specific network interface + bool sendRequest(const QNetworkInterface &networkInterface); + + // Send ARP request to a specific address within the network + bool sendRequest(const QHostAddress &targetAddress); + + bool isOpen() const; + + bool openSocket(); + void closeSocket(); + +signals: + void arpResponse(const QNetworkInterface &networkInterface, const QHostAddress &address, const QString &macAddress); + +private: + QSocketNotifier *m_socketNotifier = nullptr; + int m_socketDescriptor = -1; + bool m_isOpen = false; + + bool sendRequestInternally(int networkInterfaceIndex, const QString &senderMacAddress, const QHostAddress &senderHostAddress, const QString &targetMacAddress, const QHostAddress &targetHostAddress); + + QString getMacAddressString(uint8_t *senderHardwareAddress); + QHostAddress getHostAddressString(uint8_t *senderIpAddress); + + bool loadArpCache(const QNetworkInterface &interface = QNetworkInterface()); + + void fillMacAddress(uint8_t *targetArray, const QString &macAddress); + void fillHostAddress(uint8_t *targetArray, const QHostAddress &hostAddress); + +}; + +#endif // ARPSOCKET_H diff --git a/libnymea/network/macaddressdatabase.cpp b/libnymea/network/macaddressdatabase.cpp new file mode 100644 index 00000000..b175573b --- /dev/null +++ b/libnymea/network/macaddressdatabase.cpp @@ -0,0 +1,223 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "macaddressdatabase.h" +#include "loggingcategories.h" + +#include +#include +#include +#include +#include +#include +#include + +NYMEA_LOGGING_CATEGORY(dcMacAddressDatabase, "MacAddressDatabase") + +MacAddressDatabase::MacAddressDatabase(QObject *parent) : QObject(parent) +{ + // Find database in system data locations + QString databaseFileName; + foreach (const QString &dataLocation, QStandardPaths::standardLocations(QStandardPaths::DataLocation)) { + QFileInfo databaseFileInfo(dataLocation + QDir::separator() + "mac-addresses.db"); + if (!databaseFileInfo.exists()) { + continue; + } + + databaseFileName = databaseFileInfo.absoluteFilePath(); + break; + } + + if (databaseFileName.isEmpty()) { + qCWarning(dcMacAddressDatabase()) << "Could not find the mac address database in any system data location paths" << QStandardPaths::standardLocations(QStandardPaths::DataLocation); + qCWarning(dcMacAddressDatabase()) << "The mac address database lookup feature will not be available."; + return; + } + + + m_databaseName = databaseFileName; + + m_available = initDatabase(); + if (m_available) { + m_futureWatcher = new QFutureWatcher(this); + connect(m_futureWatcher, &QFutureWatcher::finished, this, &MacAddressDatabase::onLookupFinished); + } +} + +MacAddressDatabase::MacAddressDatabase(const QString &databaseName, QObject *parent) : + QObject(parent), + m_databaseName(databaseName) +{ + m_available = initDatabase(); + if (m_available) { + m_futureWatcher = new QFutureWatcher(this); + connect(m_futureWatcher, &QFutureWatcher::finished, this, &MacAddressDatabase::onLookupFinished); + } +} + +MacAddressDatabase::~MacAddressDatabase() +{ + m_db.close(); + m_db = QSqlDatabase(); + QSqlDatabase::removeDatabase(m_connectionName); +} + +bool MacAddressDatabase::available() const +{ + return m_available; +} + +MacAddressDatabaseReply *MacAddressDatabase::lookupMacAddress(const QString &macAddress) +{ + MacAddressDatabaseReply *reply = new MacAddressDatabaseReply(this); + connect(reply, &MacAddressDatabaseReply::finished, reply, &MacAddressDatabaseReply::deleteLater); + reply->m_macAddress = macAddress; + + if (!m_available) { + QTimer::singleShot(0, this, [=](){ emit reply->finished(); }); + return reply; + } + + m_pendingReplies.enqueue(reply); + runNextLookup(); + return reply; +} + +bool MacAddressDatabase::initDatabase() +{ + qCDebug(dcMacAddressDatabase()) << "Starting to initialize the mac address database:" << m_databaseName; + m_connectionName = QFileInfo(m_databaseName).baseName(); + m_db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), m_connectionName); + m_db.setDatabaseName(m_databaseName); + + if (!m_db.isValid()) { + qCWarning(dcMacAddressDatabase()) << "The network database is not valid" << m_db.databaseName(); + return false; + } + + m_db.close(); + if (!m_db.open()) { + qCWarning(dcMacAddressDatabase()) << "Could not open database" << m_db.databaseName() << "Initialization failed."; + return false; + } + + // Verify the tables we need exist + qCDebug(dcMacAddressDatabase()) << "Tables" << m_db.tables(); + if (!m_db.tables().contains("oui")) { + qCWarning(dcMacAddressDatabase()) << "Invalid database. Could not find \"oui\" table in" << m_db.databaseName(); + return false; + } + + if (!m_db.tables().contains("companyNames")) { + qCWarning(dcMacAddressDatabase()) << "Invalid database. Could not find \"companyNames\" table in" << m_db.databaseName(); + return false; + } + + return true; +} + +void MacAddressDatabase::runNextLookup() +{ + if (m_pendingReplies.isEmpty()) + return; + + if (m_futureWatcher->isRunning() || m_currentReply) + return; + + m_currentReply = m_pendingReplies.dequeue(); + m_currentReply->m_startTimestamp = QDateTime::currentMSecsSinceEpoch(); + QFuture future = QtConcurrent::run(this, &MacAddressDatabase::lookupMacAddressVendorInternal, m_currentReply->macAddress()); + m_futureWatcher->setFuture(future); +} + +void MacAddressDatabase::onLookupFinished() +{ + if (m_currentReply) { + QString manufacturer = m_futureWatcher->future().result(); + qCDebug(dcMacAddressDatabase()) << "Manufacturer lookup for" << m_currentReply->macAddress() << "finished:" << manufacturer << QDateTime::currentMSecsSinceEpoch() - m_currentReply->m_startTimestamp << "ms"; + m_currentReply->m_manufacturer = manufacturer; + emit m_currentReply->finished(); + m_currentReply = nullptr; + } + + runNextLookup(); +} + +QString MacAddressDatabase::lookupMacAddressVendorInternal(const QString &macAddress) +{ + qCDebug(dcMacAddressDatabase()) << "Start looking up vendor for" << macAddress; + // Convert the mac address string to upper like in the database and remove : since they have been removed for size reasons + QString fullMacAddressString = QString(macAddress).toUpper().remove(":"); + + QString manufacturer; + int length = 6; + while (true) { + QString searchString = fullMacAddressString.left(length); + QString queryString = QString("SELECT COUNT(oui) FROM oui WHERE oui LIKE \'%1%\';").arg(searchString); + qCDebug(dcMacAddressDatabase()) << "Query:" << queryString; + QSqlQuery countQuery = m_db.exec(queryString); + if (countQuery.lastError().isValid()) { + qCWarning(dcMacAddressDatabase()) << "Query finished with error" << countQuery.lastError().text(); + break; + } + + if (!countQuery.next()) + break; + + int rowCount = countQuery.value(0).toInt(); + qCDebug(dcMacAddressDatabase()) << "Found" << rowCount << "with" << searchString; + // If we have found the one... + if (rowCount == 1) { + // Query the name + queryString = QString("SELECT companyName from companyNames WHERE rowid IS (SELECT companyNameIndex FROM oui WHERE oui=\'%1\');").arg(searchString); + qCDebug(dcMacAddressDatabase()) << "Query:" << queryString; + countQuery = m_db.exec(queryString); + if (!countQuery.next()) + break; + + manufacturer = countQuery.value(0).toString(); + break; + } + + // If nothing found + if (rowCount == 0) + break; + + // Found to many results, lets add a value until we find the matching vendor + length += 1; + if (length > fullMacAddressString.length()) + break; + + // Search with one addition digit + } + + return manufacturer; +} + diff --git a/libnymea/network/macaddressdatabase.h b/libnymea/network/macaddressdatabase.h new file mode 100644 index 00000000..d661ad1d --- /dev/null +++ b/libnymea/network/macaddressdatabase.h @@ -0,0 +1,93 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef MACADDRESSDATABASE_H +#define MACADDRESSDATABASE_H + +#include +#include +#include +#include + +#include "libnymea.h" + +class LIBNYMEA_EXPORT MacAddressDatabaseReply : public QObject +{ + Q_OBJECT + friend class MacAddressDatabase; + +public: + QString macAddress() const { return m_macAddress; }; + QString manufacturer() const { return m_manufacturer; }; + +private: + explicit MacAddressDatabaseReply(QObject *parent = nullptr) : QObject(parent) { }; + QString m_macAddress; + QString m_manufacturer; + qint64 m_startTimestamp; + +signals: + void finished(); + +}; + + +class LIBNYMEA_EXPORT MacAddressDatabase : public QObject +{ + Q_OBJECT +public: + explicit MacAddressDatabase(QObject *parent = nullptr); + MacAddressDatabase(const QString &databaseName, QObject *parent = nullptr); + ~MacAddressDatabase(); + + bool available() const; + + MacAddressDatabaseReply *lookupMacAddress(const QString &macAddress); + +private: + QSqlDatabase m_db; + bool m_available = false; + QString m_connectionName; + QString m_databaseName = "/usr/share/nymea/mac-addresses.db"; + + MacAddressDatabaseReply *m_currentReply = nullptr; + QFutureWatcher *m_futureWatcher = nullptr; + QQueue m_pendingReplies; + + bool initDatabase(); + void runNextLookup(); + +private slots: + void onLookupFinished(); + QString lookupMacAddressVendorInternal(const QString &macAddress); + +}; + +#endif // MACADDRESSDATABASE_H diff --git a/libnymea/network/networkdevicediscovery.cpp b/libnymea/network/networkdevicediscovery.cpp new file mode 100644 index 00000000..54e26064 --- /dev/null +++ b/libnymea/network/networkdevicediscovery.cpp @@ -0,0 +1,267 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "networkdevicediscovery.h" +#include "loggingcategories.h" +#include "networkutils.h" +#include "macaddressdatabase.h" +#include "arpsocket.h" + +#include + +NYMEA_LOGGING_CATEGORY(dcNetworkDeviceDiscovery, "NetworkDeviceDiscovery") + +NetworkDeviceDiscovery::NetworkDeviceDiscovery(QObject *parent) : + QObject(parent) +{ + // Create ARP socket + m_arpSocket = new ArpSocket(this); + connect(m_arpSocket, &ArpSocket::arpResponse, this, &NetworkDeviceDiscovery::onArpResponseRceived); + bool arpAvailable = m_arpSocket->openSocket(); + if (!arpAvailable) { + m_arpSocket->closeSocket(); + } + + // Create ping socket + m_ping = new Ping(this); + if (!m_ping->available()) + qCWarning(dcNetworkDeviceDiscovery()) << "Failed to create ping tool" << m_ping->error(); + + // Init MAC database if available + m_macAddressDatabase = new MacAddressDatabase(this); + + // Timer for max duration af a discovery + m_discoveryTimer = new QTimer(this); + m_discoveryTimer->setInterval(20000); + m_discoveryTimer->setSingleShot(true); + connect(m_discoveryTimer, &QTimer::timeout, this, [=](){ + if (m_runningPingRepies.isEmpty() && m_currentReply) { + finishDiscovery(); + } + }); + + if (!arpAvailable && !m_ping->available()) { + qCWarning(dcNetworkDeviceDiscovery()) << "Network device discovery is not available on this system."; + } else { + qCDebug(dcNetworkDeviceDiscovery()) << "Created successfully"; + } +} + +NetworkDeviceDiscoveryReply *NetworkDeviceDiscovery::discover() +{ + if (m_currentReply) { + qCDebug(dcNetworkDeviceDiscovery()) << "Discovery already running. Returning current pending discovery reply..."; + return m_currentReply; + } + + qCDebug(dcNetworkDeviceDiscovery()) << "Starting network device discovery ..."; + NetworkDeviceDiscoveryReply *reply = new NetworkDeviceDiscoveryReply(this); + m_currentReply = reply; + m_currentReply->m_startTimestamp = QDateTime::currentMSecsSinceEpoch(); + + if (m_ping->available()) { + pingAllNetworkDevices(); + } + + if (m_arpSocket->isOpen()) { + m_arpSocket->sendRequest(); + } + + m_discoveryTimer->start(); + m_running = true; + emit runningChanged(m_running); + return reply; +} + +bool NetworkDeviceDiscovery::available() const +{ + return m_arpSocket->isOpen() || m_ping->available(); +} + +bool NetworkDeviceDiscovery::running() const +{ + return m_running; +} + +PingReply *NetworkDeviceDiscovery::ping(const QHostAddress &address) +{ + return m_ping->ping(address); +} + +MacAddressDatabaseReply *NetworkDeviceDiscovery::lookupMacAddress(const QString &macAddress) +{ + return m_macAddressDatabase->lookupMacAddress(macAddress); +} + +void NetworkDeviceDiscovery::pingAllNetworkDevices() +{ + qCDebug(dcNetworkDeviceDiscovery()) << "Starting ping for all network devices..."; + foreach (const QNetworkInterface &networkInterface, QNetworkInterface::allInterfaces()) { + if (networkInterface.flags().testFlag(QNetworkInterface::IsLoopBack)) + continue; + + if (!networkInterface.flags().testFlag(QNetworkInterface::IsUp)) + continue; + + if (!networkInterface.flags().testFlag(QNetworkInterface::IsRunning)) + continue; + + qCDebug(dcNetworkDeviceDiscovery()) << "Verifying network interface" << networkInterface.name() << networkInterface.hardwareAddress() << "..."; + foreach (const QNetworkAddressEntry &entry, networkInterface.addressEntries()) { + // Only IPv4 + if (entry.ip().protocol() != QAbstractSocket::IPv4Protocol) + continue; + + qCDebug(dcNetworkDeviceDiscovery()) << " Host address:" << entry.ip().toString(); + qCDebug(dcNetworkDeviceDiscovery()) << " Broadcast address:" << entry.broadcast().toString(); + qCDebug(dcNetworkDeviceDiscovery()) << " Netmask:" << entry.netmask().toString(); + quint32 addressRangeStart = entry.ip().toIPv4Address() & entry.netmask().toIPv4Address(); + quint32 addressRangeStop = entry.broadcast().toIPv4Address() | addressRangeStart; + quint32 range = addressRangeStop - addressRangeStart; + + // Let's scan only 255.255.255.0 networks for now + if (range > 255) + continue; + + qCDebug(dcNetworkDeviceDiscovery()) << " Address range" << range << " | from" << QHostAddress(addressRangeStart).toString() << "-->" << QHostAddress(addressRangeStop).toString(); + // Send ping request to each address within the range + for (quint32 i = 1; i < range; i++) { + quint32 address = addressRangeStart + i; + QHostAddress targetAddress(address); + + // Skip our self + if (targetAddress == entry.ip()) + continue; + + PingReply *reply = m_ping->ping(targetAddress); + m_runningPingRepies.append(reply); + connect(reply, &PingReply::finished, this, [=](){ + m_runningPingRepies.removeAll(reply); + if (reply->error() == PingReply::ErrorNoError) { + qCDebug(dcNetworkDeviceDiscovery()) << "Ping response from" << targetAddress.toString() << reply->hostName() << reply->duration() << "ms"; + int index = m_currentReply->networkDeviceInfos().indexFromHostAddress(targetAddress); + if (index < 0) { + // Add the network device + NetworkDeviceInfo networkDeviceInfo; + networkDeviceInfo.setAddress(targetAddress); + networkDeviceInfo.setHostName(reply->hostName()); + m_currentReply->networkDeviceInfos().append(networkDeviceInfo); + } else { + m_currentReply->networkDeviceInfos()[index].setAddress(targetAddress); + m_currentReply->networkDeviceInfos()[index].setHostName(reply->hostName()); + if (!m_currentReply->networkDeviceInfos()[index].networkInterface().isValid()) { + m_currentReply->networkDeviceInfos()[index].setNetworkInterface(NetworkUtils::getInterfaceForHostaddress(targetAddress)); + } + } + } + + if (m_runningPingRepies.isEmpty() && m_currentReply && !m_discoveryTimer->isActive()) { + finishDiscovery(); + } + }); + } + } + } +} + +void NetworkDeviceDiscovery::finishDiscovery() +{ + m_discoveryTimer->stop(); + m_running = false; + emit runningChanged(m_running); + + // Sort by host address + m_currentReply->networkDeviceInfos().sortNetworkDevices(); + + qint64 durationMilliSeconds = QDateTime::currentMSecsSinceEpoch() - m_currentReply->m_startTimestamp; + qCDebug(dcNetworkDeviceDiscovery()) << "Discovery finished. Found" << m_currentReply->networkDeviceInfos().count() << "network devices in" << QTime::fromMSecsSinceStartOfDay(durationMilliSeconds).toString("mm:ss.zzz"); + emit m_currentReply->finished(); + m_currentReply->deleteLater(); + m_currentReply = nullptr; +} + +void NetworkDeviceDiscovery::updateOrAddNetworkDeviceArp(const QNetworkInterface &interface, const QHostAddress &address, const QString &macAddress, const QString &manufacturer) +{ + int index = m_currentReply->networkDeviceInfos().indexFromHostAddress(address); + if (index >= 0) { + // Update the network device + m_currentReply->networkDeviceInfos()[index].setMacAddress(macAddress); + if (!manufacturer.isEmpty()) + m_currentReply->networkDeviceInfos()[index].setMacAddressManufacturer(manufacturer); + + if (interface.isValid()) { + m_currentReply->networkDeviceInfos()[index].setNetworkInterface(interface); + } + } else { + index = m_currentReply->networkDeviceInfos().indexFromMacAddress(macAddress); + if (index >= 0) { + // Update the network device + m_currentReply->networkDeviceInfos()[index].setAddress(address); + if (!manufacturer.isEmpty()) + m_currentReply->networkDeviceInfos()[index].setMacAddressManufacturer(manufacturer); + + if (interface.isValid()) { + m_currentReply->networkDeviceInfos()[index].setNetworkInterface(interface); + } + } else { + // Add the network device + NetworkDeviceInfo networkDeviceInfo; + networkDeviceInfo.setAddress(address); + networkDeviceInfo.setMacAddress(macAddress); + if (!manufacturer.isEmpty()) + networkDeviceInfo.setMacAddressManufacturer(manufacturer); + + if (interface.isValid()) + networkDeviceInfo.setNetworkInterface(interface); + + m_currentReply->networkDeviceInfos().append(networkDeviceInfo); + } + } +} + +void NetworkDeviceDiscovery::onArpResponseRceived(const QNetworkInterface &interface, const QHostAddress &address, const QString &macAddress) +{ + if (!m_currentReply) { + qCDebug(dcNetworkDeviceDiscovery()) << "Received ARP reply from" << address.toString() << macAddress << "but there is no discovery running."; + return; + } + + qCDebug(dcNetworkDeviceDiscovery()) << "ARP reply received" << address.toString() << macAddress << interface.name(); + // Lookup the mac address vendor if possible + if (m_macAddressDatabase->available()) { + MacAddressDatabaseReply *reply = m_macAddressDatabase->lookupMacAddress(macAddress); + connect(reply, &MacAddressDatabaseReply::finished, this, [=](){ + qCDebug(dcNetworkDeviceDiscovery()) << "MAC manufacturer lookup finished for" << macAddress << ":" << reply->manufacturer(); + updateOrAddNetworkDeviceArp(interface, address, macAddress, reply->manufacturer()); + }); + } else { + updateOrAddNetworkDeviceArp(interface, address, macAddress); + } +} diff --git a/libnymea/network/networkdevicediscovery.h b/libnymea/network/networkdevicediscovery.h new file mode 100644 index 00000000..712ead8c --- /dev/null +++ b/libnymea/network/networkdevicediscovery.h @@ -0,0 +1,85 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef NETWORKDEVICEDISCOVERY_H +#define NETWORKDEVICEDISCOVERY_H + +#include +#include +#include + +#include "ping.h" +#include "libnymea.h" +#include "networkdevicediscoveryreply.h" + +class ArpSocket; +class MacAddressDatabase; +class MacAddressDatabaseReply; + +Q_DECLARE_LOGGING_CATEGORY(dcNetworkDeviceDiscovery) + +class LIBNYMEA_EXPORT NetworkDeviceDiscovery : public QObject +{ + Q_OBJECT +public: + explicit NetworkDeviceDiscovery(QObject *parent = nullptr); + + NetworkDeviceDiscoveryReply *discover(); + + bool available() const; + bool running() const; + + PingReply *ping(const QHostAddress &address); + MacAddressDatabaseReply *lookupMacAddress(const QString &macAddress); + +signals: + void runningChanged(bool running); + +private: + MacAddressDatabase *m_macAddressDatabase = nullptr; + ArpSocket *m_arpSocket = nullptr; + Ping *m_ping = nullptr; + bool m_running = false; + + QTimer *m_discoveryTimer = nullptr; + NetworkDeviceDiscoveryReply *m_currentReply = nullptr; + QList m_runningPingRepies; + + void pingAllNetworkDevices(); + void finishDiscovery(); + + void updateOrAddNetworkDeviceArp(const QNetworkInterface &interface, const QHostAddress &address, const QString &macAddress, const QString &manufacturer = QString()); + +private slots: + void onArpResponseRceived(const QNetworkInterface &interface, const QHostAddress &address, const QString &macAddress); + +}; + +#endif // NETWORKDEVICEDISCOVERY_H diff --git a/libnymea/network/networkdevicediscoveryreply.cpp b/libnymea/network/networkdevicediscoveryreply.cpp new file mode 100644 index 00000000..958f1788 --- /dev/null +++ b/libnymea/network/networkdevicediscoveryreply.cpp @@ -0,0 +1,42 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "networkdevicediscoveryreply.h" + +NetworkDeviceDiscoveryReply::NetworkDeviceDiscoveryReply(QObject *parent) : + QObject(parent) +{ + +} + +NetworkDeviceInfos &NetworkDeviceDiscoveryReply::networkDeviceInfos() +{ + return m_networkDeviceInfos; +} diff --git a/libnymea/network/networkdevicediscoveryreply.h b/libnymea/network/networkdevicediscoveryreply.h new file mode 100644 index 00000000..439e949f --- /dev/null +++ b/libnymea/network/networkdevicediscoveryreply.h @@ -0,0 +1,58 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef NETWORKDEVICEDISCOVERYREPLY_H +#define NETWORKDEVICEDISCOVERYREPLY_H + +#include + +#include "libnymea.h" +#include "networkdeviceinfos.h" + +class LIBNYMEA_EXPORT NetworkDeviceDiscoveryReply : public QObject +{ + Q_OBJECT + + friend class NetworkDeviceDiscovery; + +public: + NetworkDeviceInfos &networkDeviceInfos(); + +signals: + void finished(); + +private: + explicit NetworkDeviceDiscoveryReply(QObject *parent = nullptr); + NetworkDeviceInfos m_networkDeviceInfos; + qint64 m_startTimestamp; + +}; + +#endif // NETWORKDEVICEDISCOVERYREPLY_H diff --git a/libnymea/network/networkdeviceinfo.cpp b/libnymea/network/networkdeviceinfo.cpp new file mode 100644 index 00000000..64ba1e59 --- /dev/null +++ b/libnymea/network/networkdeviceinfo.cpp @@ -0,0 +1,115 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "networkdeviceinfo.h" + +NetworkDeviceInfo::NetworkDeviceInfo() +{ + +} + +NetworkDeviceInfo::NetworkDeviceInfo(const QString &macAddress): + m_macAddress(macAddress) +{ + +} + +QString NetworkDeviceInfo::macAddress() const +{ + return m_macAddress; +} + +void NetworkDeviceInfo::setMacAddress(const QString &macAddress) +{ + m_macAddress = macAddress; +} + +QString NetworkDeviceInfo::macAddressManufacturer() const +{ + return m_macAddressManufacturer; +} + +void NetworkDeviceInfo::setMacAddressManufacturer(const QString &macAddressManufacturer) +{ + m_macAddressManufacturer = macAddressManufacturer; +} + +QHostAddress NetworkDeviceInfo::address() const +{ + return m_address; +} + +void NetworkDeviceInfo::setAddress(const QHostAddress &address) +{ + m_address = address; +} + +QString NetworkDeviceInfo::hostName() const +{ + return m_hostName; +} + +void NetworkDeviceInfo::setHostName(const QString &hostName) +{ + m_hostName = hostName; +} + +QNetworkInterface NetworkDeviceInfo::networkInterface() const +{ + return m_networkInterface; +} + +void NetworkDeviceInfo::setNetworkInterface(const QNetworkInterface &networkInterface) +{ + m_networkInterface = networkInterface; +} + +bool NetworkDeviceInfo::isValid() const +{ + return (!m_address.isNull() || !m_macAddress.isEmpty()) && m_networkInterface.isValid(); +} + +QDebug operator<<(QDebug dbg, const NetworkDeviceInfo &networkDeviceInfo) +{ + dbg.nospace() << "NetworkDeviceInfo(" << networkDeviceInfo.address().toString(); + if (!networkDeviceInfo.hostName().isEmpty()) + dbg.nospace() << " (" << networkDeviceInfo.hostName() << ")"; + + dbg.nospace() << ", " << networkDeviceInfo.macAddress(); + if (!networkDeviceInfo.macAddressManufacturer().isEmpty()) + dbg.nospace() << " (" << networkDeviceInfo.macAddressManufacturer() << ") "; + + if (networkDeviceInfo.networkInterface().isValid()) + dbg.nospace() << ", " << networkDeviceInfo.networkInterface().name(); + + dbg.nospace() << ")"; + return dbg.space(); +} + diff --git a/libnymea/network/networkdeviceinfo.h b/libnymea/network/networkdeviceinfo.h new file mode 100644 index 00000000..e6407e2b --- /dev/null +++ b/libnymea/network/networkdeviceinfo.h @@ -0,0 +1,77 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef NETWORKDEVICEINFO_H +#define NETWORKDEVICEINFO_H + +#include +#include +#include +#include + +#include "libnymea.h" + +class LIBNYMEA_EXPORT NetworkDeviceInfo +{ +public: + explicit NetworkDeviceInfo(); + explicit NetworkDeviceInfo(const QString &macAddress); + ~NetworkDeviceInfo() = default; + + QString macAddress() const; + void setMacAddress(const QString &macAddress); + + QString macAddressManufacturer() const; + void setMacAddressManufacturer(const QString &macAddressManufacturer); + + QHostAddress address() const; + void setAddress(const QHostAddress &address); + + QString hostName() const; + void setHostName(const QString &hostName); + + QNetworkInterface networkInterface() const; + void setNetworkInterface(const QNetworkInterface &networkInterface); + + bool isValid() const; + +private: + QHostAddress m_address; + QString m_macAddress; + QString m_macAddressManufacturer; + QString m_hostName; + QNetworkInterface m_networkInterface; + +}; + +QDebug operator<<(QDebug debug, const NetworkDeviceInfo &networkDeviceInfo); + + +#endif // NETWORKDEVICEINFO_H diff --git a/libnymea/network/networkdeviceinfos.cpp b/libnymea/network/networkdeviceinfos.cpp new file mode 100644 index 00000000..5d9def02 --- /dev/null +++ b/libnymea/network/networkdeviceinfos.cpp @@ -0,0 +1,113 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "networkdeviceinfos.h" + +#include + +NetworkDeviceInfos::NetworkDeviceInfos() : + QVector() +{ + +} + +NetworkDeviceInfos::NetworkDeviceInfos(const QVector &other) : + QVector(other) +{ + +} + + +int NetworkDeviceInfos::indexFromHostAddress(const QHostAddress &address) +{ + for (int i = 0; i < this->size(); i++) { + if (at(i).address().toIPv4Address() == address.toIPv4Address()) { + return i; + } + } + + return -1; +} + +int NetworkDeviceInfos::indexFromMacAddress(const QString &macAddress) +{ + for (int i = 0; i < size(); i++) { + if (at(i).macAddress().toLower() == macAddress.toLower()) { + return i; + } + } + + return -1; +} + +bool NetworkDeviceInfos::hasHostAddress(const QHostAddress &address) +{ + return indexFromHostAddress(address) >= 0; +} + +bool NetworkDeviceInfos::hasMacAddress(const QString &macAddress) +{ + return indexFromMacAddress(macAddress) >= 0; +} + +NetworkDeviceInfo NetworkDeviceInfos::get(const QHostAddress &address) +{ + foreach (const NetworkDeviceInfo &networkDeviceInfo, *this) { + if (networkDeviceInfo.address() == address) { + return networkDeviceInfo; + } + } + + return NetworkDeviceInfo(); +} + +NetworkDeviceInfo NetworkDeviceInfos::get(const QString &macAddress) +{ + foreach (const NetworkDeviceInfo &networkDeviceInfo, *this) { + if (networkDeviceInfo.macAddress() == macAddress) { + return networkDeviceInfo; + } + } + + return NetworkDeviceInfo(); +} + +void NetworkDeviceInfos::sortNetworkDevices() +{ + std::sort(this->begin(), this->end(), [](const NetworkDeviceInfo& a, const NetworkDeviceInfo& b) { + return a.address().toIPv4Address() < b.address().toIPv4Address(); + }); +} + +NetworkDeviceInfos &NetworkDeviceInfos::operator <<(const NetworkDeviceInfo &networkDeviceInfo) +{ + this->append(networkDeviceInfo); + return *this; +} diff --git a/libnymea/network/networkdeviceinfos.h b/libnymea/network/networkdeviceinfos.h new file mode 100644 index 00000000..ab23f77f --- /dev/null +++ b/libnymea/network/networkdeviceinfos.h @@ -0,0 +1,61 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef NETWORKDEVICEINFOS_H +#define NETWORKDEVICEINFOS_H + +#include + +#include "libnymea.h" +#include "networkdeviceinfo.h" + +class LIBNYMEA_EXPORT NetworkDeviceInfos : public QVector +{ + +public: + explicit NetworkDeviceInfos(); + NetworkDeviceInfos(const QVector &other); + + int indexFromHostAddress(const QHostAddress &address); + int indexFromMacAddress(const QString &macAddress); + + bool hasHostAddress(const QHostAddress &address); + bool hasMacAddress(const QString &macAddress); + + NetworkDeviceInfo get(const QHostAddress &address); + NetworkDeviceInfo get(const QString &macAddress); + + void sortNetworkDevices(); + + NetworkDeviceInfos &operator<<(const NetworkDeviceInfo &networkDeviceInfo); + +}; + +#endif // NETWORKDEVICEINFOS_H diff --git a/libnymea/network/networkutils.cpp b/libnymea/network/networkutils.cpp new file mode 100644 index 00000000..ddcf1f38 --- /dev/null +++ b/libnymea/network/networkutils.cpp @@ -0,0 +1,34 @@ +#include "networkutils.h" + +NetworkUtils::NetworkUtils() +{ + +} + +QNetworkInterface NetworkUtils::getInterfaceForHostaddress(const QHostAddress &address) +{ + foreach (const QNetworkInterface &networkInterface, QNetworkInterface::allInterfaces()) { + foreach (const QNetworkAddressEntry &entry, networkInterface.addressEntries()) { + // Only IPv4 + if (entry.ip().protocol() != QAbstractSocket::IPv4Protocol) + continue; + + if (address.isInSubnet(entry.ip(), entry.netmask().toIPv4Address())) { + return networkInterface; + } + } + } + + return QNetworkInterface(); +} + +QNetworkInterface NetworkUtils::getInterfaceForMacAddress(const QString &macAddress) +{ + foreach (const QNetworkInterface &networkInterface, QNetworkInterface::allInterfaces()) { + if (networkInterface.hardwareAddress().toLower() == macAddress.toLower()) { + return networkInterface; + } + } + + return QNetworkInterface(); +} diff --git a/libnymea/network/networkutils.h b/libnymea/network/networkutils.h new file mode 100644 index 00000000..b75aaedb --- /dev/null +++ b/libnymea/network/networkutils.h @@ -0,0 +1,46 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef NETWORKUTILS_H +#define NETWORKUTILS_H + +#include +#include + +class NetworkUtils +{ +public: + NetworkUtils(); + + static QNetworkInterface getInterfaceForHostaddress(const QHostAddress &address); + static QNetworkInterface getInterfaceForMacAddress(const QString &macAddress); +}; + +#endif // NETWORKUTILS_H diff --git a/libnymea/network/ping.cpp b/libnymea/network/ping.cpp new file mode 100644 index 00000000..36038294 --- /dev/null +++ b/libnymea/network/ping.cpp @@ -0,0 +1,422 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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) +{ + // 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_queueTimer = new QTimer(this); + m_queueTimer->setInterval(20); + m_queueTimer->setSingleShot(true); + connect(m_queueTimer, &QTimer::timeout, this, [=](){ + sendNextReply(); + }); + + 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) +{ + PingReply *reply = new PingReply(this); + reply->m_targetHostAddress = hostAddress; + reply->m_networkInterface = NetworkUtils::getInterfaceForHostaddress(hostAddress); + + // 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, this, [=]() { performPing(reply); }); +} + +void Ping::performPing(PingReply *reply) +{ + if (!m_available) { + qCDebug(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(&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(8000); + connect(reply, &PingReply::timeout, this, [=](){ + finishReply(reply, PingReply::ErrorTimeout); + }); +} + +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; +} + +void Ping::finishReply(PingReply *reply, PingReply::Error error) +{ + reply->m_error = error; + m_pendingReplies.remove(reply->requestId()); + emit reply->finished(); + reply->deleteLater(); +} + +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); + 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; + + // 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); + + qCDebug(dcPingTraffic()) << "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]"; + + } 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); + 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); +} + diff --git a/libnymea/network/ping.h b/libnymea/network/ping.h new file mode 100644 index 00000000..3ef536e2 --- /dev/null +++ b/libnymea/network/ping.h @@ -0,0 +1,109 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef PING_H +#define PING_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libnymea.h" +#include "pingreply.h" + +#include + +#define ICMP_PACKET_SIZE 64 +#define ICMP_TTL_VALUE 64 +#define ICMP_PAYLOAD_SIZE (ICMP_PACKET_SIZE - sizeof(struct icmphdr)) + +class LIBNYMEA_EXPORT Ping : public QObject +{ + Q_OBJECT +public: + explicit Ping(QObject *parent = nullptr); + + QByteArray payload() const; + void setPayload(const QByteArray &payload); + + bool available() const; + + PingReply::Error error() const; + + PingReply *ping(const QHostAddress &hostAddress); + +signals: + void availableChanged(bool available); + +private: + struct icmpPacket { + struct icmphdr icmpHeadr; + char icmpPayload[ICMP_PAYLOAD_SIZE]; + }; + + // Config + QByteArray m_payload = "ping from nymea"; + PingReply::Error m_error = PingReply::ErrorNoError; + + // Socket + QSocketNotifier *m_socketNotifier = nullptr; + int m_socketDescriptor = -1; + QHash m_pendingReplies; + bool m_available = false; + + QQueue m_replyQueue; + QTimer *m_queueTimer = nullptr; + void sendNextReply(); + QHash m_pendingHostLookups; + + //Error performPing(const QString &address); + void performPing(PingReply *reply); + void verifyErrno(int error); + + // Helper + unsigned short calculateChecksum(unsigned short *b, int len); + void cleanUpSocket(); + void timeValueSubtract(struct timeval *start, struct timeval *stop); + quint16 calculateRequestId(); + + void finishReply(PingReply *reply, PingReply::Error error); + +private slots: + void onSocketReadyRead(int socketDescriptor); + void onHostLookupFinished(const QHostInfo &info); + +}; + +#endif // PING_H diff --git a/libnymea/network/pingreply.cpp b/libnymea/network/pingreply.cpp new file mode 100644 index 00000000..c3fa499e --- /dev/null +++ b/libnymea/network/pingreply.cpp @@ -0,0 +1,74 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 "pingreply.h" + +PingReply::PingReply(QObject *parent) : QObject(parent) +{ + m_timer = new QTimer(this); + m_timer->setSingleShot(true); + connect(m_timer, &QTimer::timeout, this, &PingReply::timeout); + connect(this, &PingReply::finished, m_timer, &QTimer::stop); +} + +QHostAddress PingReply::targetHostAddress() const +{ + return m_targetHostAddress; +} + +quint16 PingReply::sequenceNumber() const +{ + return m_sequenceNumber; +} + +quint16 PingReply::requestId() const +{ + return m_requestId; +} + +QString PingReply::hostName() const +{ + return m_hostName; +} + +QNetworkInterface PingReply::networkInterface() const +{ + return m_networkInterface; +} + +double PingReply::duration() const +{ + return m_duration; +} + +PingReply::Error PingReply::error() const +{ + return m_error; +} diff --git a/libnymea/network/pingreply.h b/libnymea/network/pingreply.h new file mode 100644 index 00000000..60837181 --- /dev/null +++ b/libnymea/network/pingreply.h @@ -0,0 +1,96 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* 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 +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef PINGREPLY_H +#define PINGREPLY_H + +#include +#include +#include + +#include + +#include "libnymea.h" + +#include +#include + +class LIBNYMEA_EXPORT PingReply : public QObject +{ + Q_OBJECT + + friend class Ping; + +public: + enum Error { + ErrorNoError, + ErrorInvalidResponse, + ErrorNetworkDown, + ErrorNetworkUnreachable, + ErrorPermissionDenied, + ErrorSocketError, + ErrorTimeout, + ErrorHostUnreachable + }; + Q_ENUM(Error) + + explicit PingReply(QObject *parent = nullptr); + + QHostAddress targetHostAddress() const; + quint16 sequenceNumber() const; + quint16 requestId() const; + QString hostName() const; + QNetworkInterface networkInterface() const; + + double duration() const; + + Error error() const; + +signals: + void finished(); + void timeout(); + +private: + QTimer *m_timer = nullptr; + QHostAddress m_targetHostAddress; + quint16 m_sequenceNumber = 0; + quint16 m_requestId = 0; + QString m_hostName; + QNetworkInterface m_networkInterface; + + uint m_timeout = 3; + double m_duration = 0; + Error m_error = ErrorNoError; + + struct timeval m_startTime; + +}; + +#endif // PINGREPLY_H