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