Paste P452

Masterwork From Distant Lands
ActivePublic

Authored by davidedmundson on Tue, Aug 20, 10:48 AM.
From f73776fda75dd2ac34df2d0426bab3aba67983bd Mon Sep 17 00:00:00 2001
From: David Edmundson <kde@davidedmundson.co.uk>
Date: Mon, 19 Aug 2019 18:21:19 +0100
Subject: [PATCH] network plugin
---
cmake/FindLibcap.cmake | 59 ++++++
cmake/Findlibpcap.cmake | 110 +++++++++++
processplugins/CMakeLists.txt | 2 +
processplugins/network/CMakeLists.txt | 8 +
processplugins/network/README.md | 22 +++
processplugins/network/helper/Accumulator.cpp | 61 ++++++
processplugins/network/helper/Accumulator.h | 58 ++++++
processplugins/network/helper/CMakeLists.txt | 28 +++
processplugins/network/helper/Capture.cpp | 149 +++++++++++++++
processplugins/network/helper/Capture.h | 56 ++++++
.../network/helper/ConnectionMapping.cpp | 173 ++++++++++++++++++
.../network/helper/ConnectionMapping.h | 49 +++++
processplugins/network/helper/Packet.cpp | 153 ++++++++++++++++
processplugins/network/helper/Packet.h | 103 +++++++++++
processplugins/network/helper/TimeStamps.h | 21 +++
processplugins/network/helper/main.cpp | 69 +++++++
processplugins/network/network.cpp | 85 +++++++++
processplugins/network/network.h | 21 +++
processplugins/network/networkplugin.json | 5 +
19 files changed, 1232 insertions(+)
create mode 100644 cmake/FindLibcap.cmake
create mode 100644 cmake/Findlibpcap.cmake
create mode 100644 processplugins/CMakeLists.txt
create mode 100644 processplugins/network/CMakeLists.txt
create mode 100644 processplugins/network/README.md
create mode 100644 processplugins/network/helper/Accumulator.cpp
create mode 100644 processplugins/network/helper/Accumulator.h
create mode 100644 processplugins/network/helper/CMakeLists.txt
create mode 100644 processplugins/network/helper/Capture.cpp
create mode 100644 processplugins/network/helper/Capture.h
create mode 100644 processplugins/network/helper/ConnectionMapping.cpp
create mode 100644 processplugins/network/helper/ConnectionMapping.h
create mode 100644 processplugins/network/helper/Packet.cpp
create mode 100644 processplugins/network/helper/Packet.h
create mode 100644 processplugins/network/helper/TimeStamps.h
create mode 100644 processplugins/network/helper/main.cpp
create mode 100644 processplugins/network/network.cpp
create mode 100644 processplugins/network/network.h
create mode 100644 processplugins/network/networkplugin.json
diff --git a/cmake/FindLibcap.cmake b/cmake/FindLibcap.cmake
new file mode 100644
index 0000000..4a32446
--- /dev/null
+++ b/cmake/FindLibcap.cmake
@@ -0,0 +1,59 @@
+# Try to find the setcap binary and cap libraries
+#
+# This will define:
+#
+# Libcap_FOUND - system has the cap library and setcap binary
+# Libcap_LIBRARIES - cap libraries to link against
+# SETCAP_EXECUTABLE - path of the setcap binary
+# In addition, the following targets are defined:
+#
+# Libcap::SetCapabilities
+#
+
+
+# Copyright (c) 2014, Hrvoje Senjan, <hrvoje.senjan@gmail.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+find_program(SETCAP_EXECUTABLE NAMES setcap DOC "The setcap executable")
+
+find_library(Libcap_LIBRARIES NAMES cap DOC "The cap (capabilities) library")
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(Libcap FOUND_VAR Libcap_FOUND
+ REQUIRED_VARS SETCAP_EXECUTABLE Libcap_LIBRARIES)
+
+if(Libcap_FOUND AND NOT TARGET Libcap::SetCapabilities)
+ add_executable(Libcap::SetCapabilities IMPORTED)
+ set_target_properties(Libcap::SetCapabilities PROPERTIES
+ IMPORTED_LOCATION "${SETCAP_EXECUTABLE}"
+ )
+endif()
+
+mark_as_advanced(SETCAP_EXECUTABLE Libcap_LIBRARIES)
+
+include(FeatureSummary)
+set_package_properties(Libcap PROPERTIES
+ URL https://sites.google.com/site/fullycapable/
+ DESCRIPTION "Capabilities are a measure to limit the omnipotence of the superuser.")
diff --git a/cmake/Findlibpcap.cmake b/cmake/Findlibpcap.cmake
new file mode 100644
index 0000000..24bf6c7
--- /dev/null
+++ b/cmake/Findlibpcap.cmake
@@ -0,0 +1,110 @@
+# Copyright (c) 1995-2017, The Regents of the University of California
+# through the Lawrence Berkeley National Laboratory and the
+# International Computer Science Institute. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# (1) Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# (2) Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# (3) Neither the name of the University of California, Lawrence Berkeley
+# National Laboratory, U.S. Dept. of Energy, International Computer
+# Science Institute, nor the names of contributors may be used to endorse
+# or promote products derived from this software without specific prior
+# written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+# Note that some files in the distribution may carry their own copyright
+# notices.
+
+
+# - Try to find libpcap include dirs and libraries
+#
+# Usage of this module as follows:
+#
+# find_package(libpcap)
+#
+# Variables used by this module, they can change the default behaviour and need
+# to be set before calling find_package:
+#
+# PCAP_ROOT_DIR Set this variable to the root installation of
+# libpcap if the module has problems finding the
+# proper installation path.
+#
+# Variables defined by this module:
+#
+# PCAP_FOUND System has libpcap, include and library dirs found
+# PCAP_INCLUDE_DIR The libpcap include directories.
+# PCAP_LIBRARY The libpcap library (possibly includes a thread
+# library e.g. required by pf_ring's libpcap)
+# HAVE_PF_RING If a found version of libpcap supports PF_RING
+
+find_path(PCAP_ROOT_DIR
+ NAMES include/pcap.h
+)
+
+find_path(PCAP_INCLUDE_DIR
+ NAMES pcap.h
+ HINTS ${PCAP_ROOT_DIR}/include
+)
+
+find_library(PCAP_LIBRARY
+ NAMES pcap
+ HINTS ${PCAP_ROOT_DIR}/lib
+)
+
+include(FindPackageHandleStandardArgs)
+find_package_handle_standard_args(libpcap DEFAULT_MSG
+ PCAP_LIBRARY
+ PCAP_INCLUDE_DIR
+)
+
+include(CheckCSourceCompiles)
+set(CMAKE_REQUIRED_LIBRARIES ${PCAP_LIBRARY})
+check_c_source_compiles("int main() { return 0; }" PCAP_LINKS_SOLO)
+set(CMAKE_REQUIRED_LIBRARIES)
+
+# check if linking against libpcap also needs to link against a thread library
+if (NOT PCAP_LINKS_SOLO)
+ find_package(Threads)
+ if (THREADS_FOUND)
+ set(CMAKE_REQUIRED_LIBRARIES ${PCAP_LIBRARY} ${CMAKE_THREAD_LIBS_INIT})
+ check_c_source_compiles("int main() { return 0; }" PCAP_NEEDS_THREADS)
+ set(CMAKE_REQUIRED_LIBRARIES)
+ endif ()
+ if (THREADS_FOUND AND PCAP_NEEDS_THREADS)
+ set(_tmp ${PCAP_LIBRARY} ${CMAKE_THREAD_LIBS_INIT})
+ list(REMOVE_DUPLICATES _tmp)
+ set(PCAP_LIBRARY ${_tmp}
+ CACHE STRING "Libraries needed to link against libpcap" FORCE)
+ else ()
+ message(FATAL_ERROR "Couldn't determine how to link against libpcap")
+ endif ()
+endif ()
+
+include(CheckFunctionExists)
+set(CMAKE_REQUIRED_LIBRARIES ${PCAP_LIBRARY})
+check_function_exists(pcap_get_pfring_id HAVE_PF_RING)
+set(CMAKE_REQUIRED_LIBRARIES)
+
+mark_as_advanced(
+ PCAP_ROOT_DIR
+ PCAP_INCLUDE_DIR
+ PCAP_LIBRARY
+)
diff --git a/processplugins/CMakeLists.txt b/processplugins/CMakeLists.txt
new file mode 100644
index 0000000..82febae
--- /dev/null
+++ b/processplugins/CMakeLists.txt
@@ -0,0 +1,2 @@
+add_subdirectory(network)
+# add_subdirectory(xres)
diff --git a/processplugins/network/CMakeLists.txt b/processplugins/network/CMakeLists.txt
new file mode 100644
index 0000000..76d8ddc
--- /dev/null
+++ b/processplugins/network/CMakeLists.txt
@@ -0,0 +1,8 @@
+
+add_subdirectory(helper)
+
+add_library(ksysguard_plugin_network MODULE network.cpp)
+target_link_libraries(ksysguard_plugin_network Qt5::Core Qt5::DBus KF5::CoreAddons KF5::I18n KF5::ProcessCore)
+
+install(TARGETS ksysguard_plugin_network DESTINATION ${KDE_INSTALL_PLUGINDIR}/ksysguard/process)
+
diff --git a/processplugins/network/README.md b/processplugins/network/README.md
new file mode 100644
index 0000000..6bbaee8
--- /dev/null
+++ b/processplugins/network/README.md
@@ -0,0 +1,22 @@
+Per-process Network Usage Plugin
+================================
+
+This plugin tries to track per-process network usage and feeds that back to ksysguard.
+
+Unfortunately, at the moment there is no unpriviledged API available for this information,
+so this plugin uses a small helper application to work around that. The helper uses libpcap
+to do packet capture. To do the packet capture it needs `cap_net_raw`, but nothing else.
+
+The helper only tracks TCP and UDP traffic, on IPv4 or IPv6 networks. Only the beginning of
+each packet is captured, so we only get the packet headers. These are processed to extract
+the source and destination IP address and port, which are matched with sockets and processes.
+
+The matching uses information parsed from /proc/net/tcp{,6} and /proc/net/udp{,6} for the
+sockets, which are mapped to processes by listing fds from /proc/${pid}/fd/ and reading their
+symlink targets. Entries matching "socket:[${port}]" are used to track socket to process
+mapping.
+
+Once mapped, we store how much data was received for each process by accumulating the packet
+sizes for each socket. Every second this information is printed to the helper's stdout using
+the format 00:00:00|PID|0000|IN|000|OUT|000 or just 00:00:00 if there was no data that second.
+The helper's stdout is read and parsed by the network plugin and fed into the ksysguard backend.
diff --git a/processplugins/network/helper/Accumulator.cpp b/processplugins/network/helper/Accumulator.cpp
new file mode 100644
index 0000000..c79d699
--- /dev/null
+++ b/processplugins/network/helper/Accumulator.cpp
@@ -0,0 +1,61 @@
+#include "Accumulator.h"
+
+#include "Capture.h"
+#include "ConnectionMapping.h"
+#include "Packet.h"
+
+#include <QDebug>
+
+using namespace std::chrono_literals;
+
+Accumulator::Accumulator(std::shared_ptr<Capture> capture, std::shared_ptr<ConnectionMapping> mapping, QObject *parent)
+ : QObject(parent)
+{
+ m_capture = capture;
+ m_mapping = mapping;
+
+ m_running = true;
+ m_thread = std::thread { &Accumulator::loop, this };
+}
+
+Accumulator::~Accumulator()
+{
+}
+
+Accumulator::PidDataCounterHash Accumulator::data()
+{
+ auto tmp = m_data;
+ m_data.clear();
+ return tmp;
+}
+
+void Accumulator::stop()
+{
+ m_running = false;
+}
+
+void Accumulator::loop()
+{
+ while (m_running) {
+ auto packet = m_capture->nextPacket();
+
+ auto result = m_mapping->pidForPacket(packet);
+ if (result.pid == 0)
+ continue;
+
+ addData(result.direction, packet, result.pid);
+ }
+}
+
+void Accumulator::addData(Packet::Direction direction, const Packet &packet, int pid)
+{
+ if (!m_data.contains(pid)) {
+ m_data.insert(pid, InboundOutboundData { 0, 0 });
+ }
+
+ if (direction == Packet::Direction::Inbound) {
+ m_data[pid].first += packet.size();
+ } else {
+ m_data[pid].second += packet.size();
+ };
+}
diff --git a/processplugins/network/helper/Accumulator.h b/processplugins/network/helper/Accumulator.h
new file mode 100644
index 0000000..025891a
--- /dev/null
+++ b/processplugins/network/helper/Accumulator.h
@@ -0,0 +1,58 @@
+#ifndef ACCUMULATOR_H
+#define ACCUMULATOR_H
+
+#include <memory>
+#include <thread>
+#include <atomic>
+
+#include "TimeStamps.h"
+#include "Packet.h"
+
+#include <QHash>
+#include <QObject>
+
+class Capture;
+class ConnectionMapping;
+class Packet;
+
+/**
+ * @todo write docs
+ */
+class Accumulator : public QObject
+{
+ Q_OBJECT
+
+public:
+ using InboundOutboundData = QPair<int, int>;
+ using PidDataCounterHash = QHash<int, InboundOutboundData>;
+
+ /**
+ * Constructor
+ *
+ * @param parent TODO
+ */
+ Accumulator(std::shared_ptr<Capture> capture, std::shared_ptr<ConnectionMapping> mapping, QObject *parent = nullptr);
+
+ /**
+ * Destructor
+ */
+ ~Accumulator();
+
+ PidDataCounterHash data();
+
+ void stop();
+
+private:
+ void addData(Packet::Direction direction, const Packet &packet, int pid);
+ void loop();
+
+ std::shared_ptr<Capture> m_capture;
+ std::shared_ptr<ConnectionMapping> m_mapping;
+
+ std::thread m_thread;
+ std::atomic_bool m_running;
+
+ PidDataCounterHash m_data;
+};
+
+#endif // ACCUMULATOR_H
diff --git a/processplugins/network/helper/CMakeLists.txt b/processplugins/network/helper/CMakeLists.txt
new file mode 100644
index 0000000..23d622b
--- /dev/null
+++ b/processplugins/network/helper/CMakeLists.txt
@@ -0,0 +1,28 @@
+
+set(ksgrd_network_helper_SRCS
+ main.cpp
+ Capture.cpp
+ Packet.cpp
+ ConnectionMapping.cpp
+ Accumulator.cpp
+)
+
+add_executable(ksgrd_network_helper ${ksgrd_network_helper_SRCS})
+target_include_directories(ksgrd_network_helper PUBLIC ${PCAP_INCLUDE_DIR})
+target_link_libraries(ksgrd_network_helper Qt5::Core ${PCAP_LIBRARY})
+kde_target_enable_exceptions(ksgrd_network_helper PUBLIC)
+
+# Why can't CMake fix this itself?'
+target_link_libraries(ksgrd_network_helper pthread)
+
+install(TARGETS ksgrd_network_helper DESTINATION ${KDE_INSTALL_BINDIR})
+
+if (HAVE_LIBCAP)
+ install(
+ CODE "execute_process(
+ COMMAND
+ ${SETCAP_EXECUTABLE}
+ CAP_NET_RAW=+ep
+ \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/ksgrd_network_helper)"
+ )
+endif()
diff --git a/processplugins/network/helper/Capture.cpp b/processplugins/network/helper/Capture.cpp
new file mode 100644
index 0000000..3687997
--- /dev/null
+++ b/processplugins/network/helper/Capture.cpp
@@ -0,0 +1,149 @@
+#include "Capture.h"
+
+#include <iostream>
+
+#include <pcap/pcap.h>
+
+#include "Packet.h"
+#include "TimeStamps.h"
+
+void pcapDispatchCallback(uchar *user, const struct pcap_pkthdr *h, const uchar *bytes)
+{
+ reinterpret_cast<Capture *>(user)->handlePacket(h, bytes);
+}
+
+Capture::Capture(const QString &interface, QObject *parent)
+ : QObject(parent)
+{
+ m_interface = interface;
+}
+
+Capture::~Capture()
+{
+ if (m_pcap) {
+ if (m_active) {
+ stop();
+ }
+
+ pcap_close(m_pcap);
+ }
+}
+
+bool Capture::start()
+{
+ auto device = m_interface.isEmpty() ? (const char *)nullptr : m_interface.toLatin1().constData();
+
+ char errorBuffer[PCAP_ERRBUF_SIZE];
+ m_pcap = pcap_create(device, errorBuffer);
+ if (!m_pcap) {
+ m_error = QString::fromLatin1(errorBuffer);
+ return false;
+ }
+
+ pcap_set_timeout(m_pcap, 500);
+ pcap_set_snaplen(m_pcap, 100);
+ pcap_set_promisc(m_pcap, 0);
+ pcap_set_datalink(m_pcap, DLT_LINUX_SLL);
+
+ if (checkError(pcap_activate(m_pcap)))
+ return false;
+
+ struct bpf_program filter;
+ if (checkError(pcap_compile(m_pcap, &filter, "tcp or udp", 1, PCAP_NETMASK_UNKNOWN))) {
+ pcap_freecode(&filter);
+ return false;
+ }
+
+ if (checkError(pcap_setfilter(m_pcap, &filter))) {
+ pcap_freecode(&filter);
+ return false;
+ }
+
+ pcap_freecode(&filter);
+
+ m_thread = std::thread { &Capture::loop, this };
+
+ return true;
+}
+
+void Capture::stop()
+{
+ pcap_breakloop(m_pcap);
+ if (m_thread.joinable()) {
+ m_thread.join();
+ }
+}
+
+QString Capture::lastError() const
+{
+ return m_error;
+}
+
+void Capture::reportStatistics()
+{
+ pcap_stat stats;
+ pcap_stats(m_pcap, &stats);
+
+ std::cout << "Packet Statistics: " << std::endl;
+ std::cout << " " << stats.ps_recv << " received" << std::endl;
+ std::cout << " " << stats.ps_drop << " dropped (full)" << std::endl;
+ std::cout << " " << stats.ps_ifdrop << " dropped (iface)" << std::endl;
+ std::cout << " " << m_packetCount << " processed" << std::endl;
+}
+
+Packet Capture::nextPacket()
+{
+ std::unique_lock<std::mutex> lock(m_mutex);
+ m_condition.wait(lock, [this]() { return m_queue.size() > 0; });
+
+ auto packet = std::move(m_queue.front());
+ m_queue.pop_front();
+ return packet;
+}
+
+void Capture::loop()
+{
+ pcap_loop(m_pcap, -1, &pcapDispatchCallback, reinterpret_cast<uchar *>(this));
+}
+
+bool Capture::checkError(int result)
+{
+ switch (result) {
+ case PCAP_ERROR_ACTIVATED:
+ m_error = QStringLiteral("The handle has already been activated");
+ return true;
+ case PCAP_ERROR_NO_SUCH_DEVICE:
+ m_error = QStringLiteral("The capture source specified when the handle was created doesn't exist");
+ return true;
+ case PCAP_ERROR_PERM_DENIED:
+ m_error = QStringLiteral("The process doesn't have permission to open the capture source");
+ return true;
+ case PCAP_ERROR_PROMISC_PERM_DENIED:
+ m_error = QStringLiteral("The process has permission to open the capture source but doesn't have permission to put it into promiscuous mode");
+ return true;
+ case PCAP_ERROR_RFMON_NOTSUP:
+ m_error = QStringLiteral("Monitor mode was specified but the capture source doesn't support monitor mode");
+ return true;
+ case PCAP_ERROR_IFACE_NOT_UP:
+ m_error = QStringLiteral("The capture source device is not up");
+ return true;
+ case PCAP_ERROR:
+ m_error = QString::fromLatin1(pcap_geterr(m_pcap));
+ return true;
+ }
+
+ return false;
+}
+
+void Capture::handlePacket(const struct pcap_pkthdr *header, const uchar *data)
+{
+ auto timeStamp = std::chrono::time_point_cast<TimeStamp::MicroSeconds::duration>(std::chrono::system_clock::from_time_t(header->ts.tv_sec) + std::chrono::microseconds { header->ts.tv_usec });
+
+ m_packetCount++;
+ {
+ std::lock_guard<std::mutex> lock { m_mutex };
+ m_queue.emplace_back(timeStamp, data, header->caplen, header->len);
+ }
+
+ m_condition.notify_all();
+}
diff --git a/processplugins/network/helper/Capture.h b/processplugins/network/helper/Capture.h
new file mode 100644
index 0000000..eb6cfea
--- /dev/null
+++ b/processplugins/network/helper/Capture.h
@@ -0,0 +1,56 @@
+#ifndef CAPTURE_H
+#define CAPTURE_H
+
+#include <thread>
+#include <mutex>
+#include <condition_variable>
+#include <deque>
+
+#include <QObject>
+
+class pcap;
+class Packet;
+
+/**
+ * @todo write docs
+ */
+class Capture : public QObject
+{
+ Q_OBJECT
+public:
+ /**
+ * Default constructor
+ */
+ Capture(const QString &interface = QString {}, QObject *parent = nullptr);
+
+ /**
+ * Destructor
+ */
+ ~Capture();
+
+ bool start();
+ void stop();
+ QString lastError() const;
+ void reportStatistics();
+ Packet nextPacket();
+
+ void handlePacket(const struct pcap_pkthdr *header, const uchar *data);
+
+private:
+ void loop();
+ bool checkError(int result);
+
+ QString m_interface;
+ QString m_error;
+ std::atomic_bool m_active;
+ std::thread m_thread;
+ std::mutex m_mutex;
+ std::condition_variable m_condition;
+ std::deque<Packet> m_queue;
+
+ int m_packetCount = 0;
+
+ pcap *m_pcap;
+};
+
+#endif // CAPTURE_H
diff --git a/processplugins/network/helper/ConnectionMapping.cpp b/processplugins/network/helper/ConnectionMapping.cpp
new file mode 100644
index 0000000..a0ef687
--- /dev/null
+++ b/processplugins/network/helper/ConnectionMapping.cpp
@@ -0,0 +1,173 @@
+#include "ConnectionMapping.h"
+
+#include <fstream>
+#include <iostream>
+
+#include <dirent.h>
+#include <errno.h>
+#include <unistd.h>
+
+using namespace std::string_literals;
+
+// Convert /proc/net/tcp's mangled big-endian notation to a host-endian int32'
+uint32_t tcpToInt(const QStringRef &part)
+{
+ uint32_t result = 0;
+ result |= part.mid(0, 2).toInt(nullptr, 16) << 24;
+ result |= part.mid(2, 2).toInt(nullptr, 16) << 16;
+ result |= part.mid(4, 2).toInt(nullptr, 16) << 8;
+ result |= part.mid(6, 2).toInt(nullptr, 16) << 0;
+ return result;
+}
+
+ConnectionMapping::ConnectionMapping()
+{
+ m_socketFileMatch =
+ // Format of /proc/net/tcp is:
+ // sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
+ // 0: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 31896 ...
+ // Where local_address is a hex representation of the IP Address and port, in big endian notation.
+ // Since we care only about local address, local port and inode we ignore the middle 70 characters.
+ QRegularExpression { "\\s*\\d+: (?:(\\w{8})|(\\w{32})):([A-F0-9]{4}) (.{94}|.{70}) (\\d+) .*" };
+
+ parseProc();
+}
+
+ConnectionMapping::~ConnectionMapping()
+{
+}
+
+ConnectionMapping::PacketResult ConnectionMapping::pidForPacket(const Packet &packet)
+{
+ PacketResult result;
+
+ auto sourceInode = m_localToINode.find(packet.sourceAddress());
+ auto destInode = m_localToINode.find(packet.destinationAddress());
+
+ if (sourceInode == m_localToINode.end() && destInode == m_localToINode.end()) {
+ parseProc();
+
+ sourceInode = m_localToINode.find(packet.sourceAddress());
+ destInode = m_localToINode.find(packet.destinationAddress());
+
+ if (sourceInode == m_localToINode.end() && destInode == m_localToINode.end()) {
+ return result;
+ }
+ }
+
+ auto inode = m_localToINode.end();
+ if (sourceInode != m_localToINode.end()) {
+ result.direction = Packet::Direction::Outbound;
+ inode = sourceInode;
+ } else {
+ result.direction = Packet::Direction::Inbound;
+ inode = destInode;
+ }
+
+ auto pid = m_inodeToPid.find((*inode).second);
+ if (pid == m_inodeToPid.end()) {
+ result.pid = -1;
+ } else {
+ result.pid = (*pid).second;
+ }
+ return result;
+}
+
+void ConnectionMapping::parseProc()
+{
+ //TODO: Consider using INET_DIAG netlink protocol for retrieving socket information.
+ if (parseSockets())
+ parsePid();
+}
+
+bool ConnectionMapping::parseSockets()
+{
+ auto oldInodes = m_inodes;
+
+ m_inodes.clear();
+ m_localToINode.clear();
+ parseSocketFile("/proc/net/tcp");
+ parseSocketFile("/proc/net/udp");
+ parseSocketFile("/proc/net/tcp6");
+ parseSocketFile("/proc/net/udp6");
+
+ if (m_inodes == oldInodes) {
+ return false;
+ }
+
+ return true;
+}
+
+void ConnectionMapping::parsePid()
+{
+ std::unordered_set<int> pids;
+
+ auto dir = opendir("/proc");
+ dirent *entry = nullptr;
+ while ((entry = readdir(dir)))
+ if (entry->d_name[0] >= '0' && entry->d_name[0] <= '9')
+ pids.insert(std::stoi(entry->d_name));
+ closedir(dir);
+
+ char buffer[100] = { "\0" };
+ m_inodeToPid.clear();
+ for (auto pid : pids) {
+ auto fdPath = "/proc/%/fd"s.replace(6, 1, std::to_string(pid));
+ auto dir = opendir(fdPath.data());
+ if (dir == NULL) {
+ continue;
+ }
+
+ dirent *fd = nullptr;
+ while ((fd = readdir(dir))) {
+ memset(buffer, 0, 100);
+ readlinkat(dirfd(dir), fd->d_name, buffer, 100);
+ auto target = std::string(buffer);
+ if (target.find("socket:") == std::string::npos)
+ continue;
+
+ auto inode = std::stoi(target.substr(8));
+ m_inodeToPid.insert(std::make_pair(inode, pid));
+ }
+
+ closedir(dir);
+ }
+}
+
+void ConnectionMapping::parseSocketFile(const char *fileName)
+{
+ std::ifstream file { fileName };
+ if (!file.is_open())
+ return;
+
+ std::string data;
+ while (std::getline(file, data)) {
+ auto match = m_socketFileMatch.match(QString::fromStdString(data));
+ if (!match.hasMatch())
+ continue;
+
+ Packet::Address localAddress;
+ if (!match.capturedRef(1).isEmpty()) {
+ localAddress.address[3] = tcpToInt(match.capturedRef(1));
+ } else {
+ auto ipv6 = match.capturedRef(2);
+ if (ipv6.startsWith(QStringLiteral("0000000000000000FFFF0000"))) {
+ // Some applications (like Steam) use some form of ipv4-over-ipv6.
+ // They use ipv4 addresses that end up in the tcp6 file.
+ // They seems to start with 0000000000000000FFFF0000, so if we
+ // detect that, assume it is ipv4-over-ipv6.
+ localAddress.address[3] = tcpToInt(ipv6.mid(24, 8));
+ } else {
+ localAddress.address[0] = tcpToInt(ipv6.mid(0, 8));
+ localAddress.address[1] = tcpToInt(ipv6.mid(8, 8));
+ localAddress.address[2] = tcpToInt(ipv6.mid(16, 8));
+ localAddress.address[3] = tcpToInt(ipv6.mid(24, 8));
+ }
+ }
+
+ localAddress.port = match.capturedRef(3).toInt(nullptr, 16);
+ auto inode = match.capturedRef(5).toInt();
+ m_localToINode.insert(std::make_pair(localAddress, inode));
+ m_inodes.insert(inode);
+ }
+}
diff --git a/processplugins/network/helper/ConnectionMapping.h b/processplugins/network/helper/ConnectionMapping.h
new file mode 100644
index 0000000..0017d66
--- /dev/null
+++ b/processplugins/network/helper/ConnectionMapping.h
@@ -0,0 +1,49 @@
+#ifndef CONNECTIONMAPPING_H
+#define CONNECTIONMAPPING_H
+
+#include <unordered_map>
+#include <unordered_set>
+
+#include <QRegularExpression>
+
+#include "Packet.h"
+
+/**
+ * @todo write docs
+ */
+class ConnectionMapping
+{
+public:
+ struct PacketResult {
+ int pid = 0;
+ Packet::Direction direction;
+ };
+
+ /**
+ * Default constructor
+ */
+ ConnectionMapping();
+
+ /**
+ * Destructor
+ */
+ ~ConnectionMapping();
+
+ void parseProc();
+
+ PacketResult pidForPacket(const Packet &packet);
+
+private:
+ bool parseSockets();
+ void parsePid();
+ void parseSocketFile(const char* fileName);
+
+ std::unordered_map<Packet::Address, int> m_localToINode;
+ std::unordered_map<int, int> m_inodeToPid;
+ std::unordered_set<int> m_inodes;
+ std::unordered_set<int> m_pids;
+
+ QRegularExpression m_socketFileMatch;
+};
+
+#endif // CONNECTIONMAPPING_H
diff --git a/processplugins/network/helper/Packet.cpp b/processplugins/network/helper/Packet.cpp
new file mode 100644
index 0000000..6ba69d5
--- /dev/null
+++ b/processplugins/network/helper/Packet.cpp
@@ -0,0 +1,153 @@
+#include "Packet.h"
+
+#include <arpa/inet.h>
+#include <linux/if_ether.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/tcp.h>
+#include <netinet/udp.h>
+
+#include <pcap/pcap.h>
+#include <pcap/sll.h>
+
+#include <QDebug>
+
+Packet::Packet()
+{
+}
+
+Packet::Packet(const TimeStamp::MicroSeconds &timeStamp, const uint8_t *data, uint32_t dataLength, uint32_t packetSize)
+{
+ m_timeStamp = timeStamp;
+ m_size = packetSize;
+
+ const sll_header *header = reinterpret_cast<const sll_header *>(data);
+ switch (ntohs(header->sll_protocol)) {
+ case ETH_P_IP:
+ m_networkProtocol = NetworkProtocolType::IPv4;
+ parseIPv4(data + sizeof(sll_header));
+ break;
+ case ETH_P_IPV6:
+ m_networkProtocol = NetworkProtocolType::IPv6;
+ parseIPv6(data + sizeof(sll_header));
+ break;
+ default:
+ m_networkProtocol = NetworkProtocolType::Unknown;
+ break;
+ }
+}
+
+Packet::~Packet()
+{
+}
+
+unsigned int Packet::size() const
+{
+ return m_size;
+}
+
+TimeStamp::MicroSeconds Packet::timeStamp() const
+{
+ return m_timeStamp;
+}
+
+Packet::NetworkProtocolType Packet::networkProtocol() const
+{
+ return m_networkProtocol;
+}
+
+Packet::TransportProtocolType Packet::transportProtocol() const
+{
+ return m_transportProtocol;
+}
+
+Packet::Address Packet::sourceAddress() const
+{
+ return m_sourceAddress;
+}
+
+Packet::Address Packet::destinationAddress() const
+{
+ return m_destinationAddress;
+}
+
+void Packet::parseIPv4(const uint8_t *data)
+{
+ const ip *header = reinterpret_cast<const ip *>(data);
+
+ m_sourceAddress.address[3] = header->ip_src.s_addr;
+ m_destinationAddress.address[3] = header->ip_dst.s_addr;
+
+ parseTransport(header->ip_p, data + sizeof(ip));
+}
+
+void Packet::parseIPv6(const uint8_t *data)
+{
+ const ip6_hdr *header = reinterpret_cast<const ip6_hdr *>(data);
+
+ m_sourceAddress.address = {
+ header->ip6_src.s6_addr32[0],
+ header->ip6_src.s6_addr32[1],
+ header->ip6_src.s6_addr32[2],
+ header->ip6_src.s6_addr32[3]
+ };
+ m_destinationAddress.address = {
+ header->ip6_dst.s6_addr32[0],
+ header->ip6_dst.s6_addr32[1],
+ header->ip6_dst.s6_addr32[2],
+ header->ip6_dst.s6_addr32[3]
+ };
+
+ parseTransport(header->ip6_nxt, data + sizeof(ip6_hdr));
+}
+
+void Packet::parseTransport(uint8_t type, const uint8_t *data)
+{
+ switch (type) {
+ case IPPROTO_TCP: {
+ m_transportProtocol = TransportProtocolType::Tcp;
+ const tcphdr *tcpHeader = reinterpret_cast<const tcphdr *>(data);
+ m_sourceAddress.port = ntohs(tcpHeader->th_sport);
+ m_destinationAddress.port = ntohs(tcpHeader->th_dport);
+ break;
+ }
+ case IPPROTO_UDP: {
+ m_transportProtocol = TransportProtocolType::Udp;
+ const udphdr *udpHeader = reinterpret_cast<const udphdr *>(data);
+ m_sourceAddress.port = ntohs(udpHeader->uh_sport);
+ m_destinationAddress.port = ntohs(udpHeader->uh_dport);
+ break;
+ }
+ default:
+ m_transportProtocol = TransportProtocolType::Unknown;
+ break;
+ }
+}
+
+QDebug operator<<(QDebug stream, const Packet &packet)
+{
+ stream << "Packet"
+ << packet.size() << "bytes"
+ << packet.timeStamp().time_since_epoch().count()
+ << int(packet.networkProtocol())
+ << int(packet.transportProtocol())
+ << packet.sourceAddress()
+ << packet.destinationAddress();
+ return stream;
+}
+
+QDebug operator<<(QDebug stream, const Packet::Address &address)
+{
+ QDebugStateSaver saver { stream };
+ if (address.address[0] == 0 && address.address[1] == 0 && address.address[2] == 0) {
+ char buffer[INET_ADDRSTRLEN];
+ inet_ntop(AF_INET, &address.address[3], buffer, INET_ADDRSTRLEN);
+ stream.nospace() << buffer;
+ } else {
+ char buffer[INET6_ADDRSTRLEN];
+ inet_ntop(AF_INET6, &address.address, buffer, INET6_ADDRSTRLEN);
+ stream.nospace() << buffer;
+ }
+ stream.nospace() << ":" << dec << address.port;
+ return stream;
+}
diff --git a/processplugins/network/helper/Packet.h b/processplugins/network/helper/Packet.h
new file mode 100644
index 0000000..78b0bd3
--- /dev/null
+++ b/processplugins/network/helper/Packet.h
@@ -0,0 +1,103 @@
+#ifndef PACKET_H
+#define PACKET_H
+
+#include <chrono>
+#include <cstdint>
+#include <array>
+#include <functional>
+
+#include "TimeStamps.h"
+
+class QDebug;
+
+/**
+ * @todo write docs
+ */
+class Packet
+{
+public:
+ enum class NetworkProtocolType {
+ Unknown,
+ IPv4,
+ IPv6,
+ };
+
+ enum class TransportProtocolType {
+ Unknown,
+ Tcp,
+ Udp,
+ };
+
+ enum class Direction {
+ Inbound,
+ Outbound,
+ };
+
+ struct Address
+ {
+ std::array<uint32_t, 4> address = { 0 };
+ uint32_t port = 0;
+
+ bool operator==(const Address &other) const
+ {
+ return address == other.address
+ && port == other.port;
+ }
+ };
+
+ /**
+ * Default constructor
+ */
+ Packet();
+
+ Packet(const TimeStamp::MicroSeconds &timeStamp, const uint8_t *data, uint32_t dataLength, uint32_t packetSize);
+
+ /**
+ * Destructor
+ */
+ ~Packet();
+
+ Packet(const Packet &other) = delete;
+ Packet(Packet &&other) = default;
+
+ TimeStamp::MicroSeconds timeStamp() const;
+ unsigned int size() const;
+ NetworkProtocolType networkProtocol() const;
+ TransportProtocolType transportProtocol() const;
+ Address sourceAddress() const;
+ Address destinationAddress() const;
+
+private:
+ void parseIPv4(const uint8_t *data);
+ void parseIPv6(const uint8_t *data);
+ void parseTransport(uint8_t type, const uint8_t *data);
+
+ TimeStamp::MicroSeconds m_timeStamp;
+ unsigned int m_size = 0;
+
+ NetworkProtocolType m_networkProtocol = NetworkProtocolType::Unknown;
+ TransportProtocolType m_transportProtocol = TransportProtocolType::Unknown;
+
+ Address m_sourceAddress;
+ Address m_destinationAddress;
+};
+
+QDebug operator<<(QDebug stream, const Packet &packet);
+QDebug operator<<(QDebug stream, const Packet::Address &address);
+
+namespace std {
+ template <> struct hash<Packet::Address>
+ {
+ using argument_type = Packet::Address;
+ using result_type = std::size_t;
+ inline result_type operator()(argument_type const& address) const noexcept {
+ return std::hash<uint32_t>{}(address.address[0])
+ ^ (std::hash<uint32_t>{}(address.address[1]) << 1)
+ ^ (std::hash<uint32_t>{}(address.address[2]) << 2)
+ ^ (std::hash<uint32_t>{}(address.address[3]) << 3)
+ ^ (std::hash<uint32_t>{}(address.port) << 4);
+ }
+ };
+}
+
+#endif // PACKET_H
diff --git a/processplugins/network/helper/TimeStamps.h b/processplugins/network/helper/TimeStamps.h
new file mode 100644
index 0000000..0a51368
--- /dev/null
+++ b/processplugins/network/helper/TimeStamps.h
@@ -0,0 +1,21 @@
+#ifndef TIMESTAMPS_H
+#define TIMESTAMPS_H
+
+#include <chrono>
+
+// This is a helper header to simplify some of the std::chrono usages.
+// In addition, this contains a qHash implementation for std::chrono.
+// Note that to use the qHash, this file needs to be included before <QHash>
+
+namespace TimeStamp
+{
+using MicroSeconds = std::chrono::time_point<std::chrono::system_clock, std::chrono::microseconds>;
+using Seconds = std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>;
+}
+
+inline unsigned int qHash(const TimeStamp::Seconds &timeStamp, unsigned int seed = 0)
+{
+ return timeStamp.time_since_epoch().count() ^ seed;
+}
+
+#endif
diff --git a/processplugins/network/helper/main.cpp b/processplugins/network/helper/main.cpp
new file mode 100644
index 0000000..554c86f
--- /dev/null
+++ b/processplugins/network/helper/main.cpp
@@ -0,0 +1,69 @@
+#include <iomanip>
+#include <iostream>
+#include <thread>
+
+#include <QCommandLineParser>
+#include <QCoreApplication>
+#include <QDebug>
+#include <QTimer>
+
+#include "Accumulator.h"
+#include "Capture.h"
+#include "ConnectionMapping.h"
+#include "Packet.h"
+#include "TimeStamps.h"
+
+int main(int argc, char **argv)
+{
+ QCoreApplication app(argc, argv);
+
+ QCommandLineParser parser;
+ parser.addOption({ "stats", "Print packet capture statistics" });
+ parser.setApplicationDescription("Helper application for tracking per-process network usage.");
+ parser.addHelpOption();
+ parser.process(app);
+
+ auto mapping = std::make_shared<ConnectionMapping>();
+
+ auto capture = std::make_shared<Capture>();
+ if (!capture->start()) {
+ qWarning() << capture->lastError();
+ return 1;
+ }
+
+ auto accumulator = std::make_shared<Accumulator>(capture, mapping);
+
+ TimeStamp::Seconds lastDisplayedStamp;
+
+ QTimer timer;
+ timer.setInterval(1000);
+ QObject::connect(&timer, &QTimer::timeout, [&]() {
+ lastDisplayedStamp = std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::system_clock::now());
+
+ auto data = accumulator->data();
+ auto timeStamp = std::chrono::system_clock::to_time_t(lastDisplayedStamp);
+
+ if (parser.isSet("stats")) {
+ capture->reportStatistics();
+ }
+
+ if (data.isEmpty()) {
+ std::cout << std::put_time(std::localtime(&timeStamp), "%T") << std::endl;
+ return;
+ }
+
+ for (auto itr = data.begin(); itr != data.end(); ++itr) {
+ std::cout << std::put_time(std::localtime(&timeStamp), "%T");
+ std::cout << "|PID|" << itr.key() << "|IN|" << itr.value().first << "|OUT|" << itr.value().second;
+ std::cout << std::endl;
+ }
+ });
+ timer.start();
+
+ auto result = app.exec();
+
+ accumulator->stop();
+ capture->stop();
+
+ return result;
+}
diff --git a/processplugins/network/network.cpp b/processplugins/network/network.cpp
new file mode 100644
index 0000000..eb2c816
--- /dev/null
+++ b/processplugins/network/network.cpp
@@ -0,0 +1,85 @@
+#include "network.h"
+
+#include <QDateTime>
+#include <QDebug>
+#include <QHash>
+#include <QProcess>
+#include <QStandardPaths>
+
+#include <KLocalizedString>
+#include <KPluginFactory>
+
+#include <process_data_provider.h>
+
+using namespace KSysGuard;
+
+NetworkPlugin::NetworkPlugin(QObject *parent, const QVariantList &args)
+ : ProcessDataProvider(parent, args)
+{
+ const auto executable = QStandardPaths::findExecutable("ksgrd_network_helper");
+ if (executable.isEmpty()) {
+ qWarning() << "Could not find ksgrd_network_helper";
+ return;
+ }
+
+ m_inboundSensor = new ProcessAttribute("netInbound", i18n("Download"), this);
+ m_inboundSensor->setUnit(KSysGuard::UnitByteRate);
+ m_outboundSensor = new ProcessAttribute("netOutbound", i18n("Upload"), this);
+ m_outboundSensor->setUnit(KSysGuard::UnitByteRate);
+
+ addProcessAttribute(m_inboundSensor);
+ addProcessAttribute(m_outboundSensor);
+
+ m_process = new QProcess(this);
+ m_process->setProgram(executable);
+
+ connect(m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), [=](int exitCode, QProcess::ExitStatus status) {
+ if (exitCode != 0 || status != QProcess::NormalExit) {
+ qWarning() << m_process->readAllStandardOutput();
+ }
+ });
+
+ connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() {
+ while (m_process->canReadLine()) {
+ const QString line = m_process->readLine();
+
+ // Each line consists of: timestamp|PID|pid|IN|in_bytes|OUT|out_bytes
+ const auto parts = line.splitRef(QLatin1Char('|'), QString::SkipEmptyParts);
+ if (parts.size() < 2)
+ continue;
+
+ long pid = parts.at(2).toLong();
+
+ auto timeStamp = QDateTime::currentDateTimeUtc();
+ timeStamp.setTime(QTime::fromString(parts.at(0).toString(), "HH:mm:ss"));
+
+ auto bytesIn = parts.at(4).toUInt();
+ auto bytesOut = parts.at(6).toUInt();
+
+ auto process = getProcess(pid);
+ if (!process) {
+ return;
+ }
+
+ if (bytesIn > 0) { //David - why??
+ m_inboundSensor->setData(process, bytesIn);
+ }
+ if (bytesOut > 0) {
+ m_outboundSensor->setData(process, bytesOut);
+ }
+ }
+ });
+}
+
+void NetworkPlugin::handleEnabledChanged(bool enabled)
+{
+ if (enabled) {
+ m_process->start();
+ } else{
+ m_process->terminate();
+ }
+}
+
+K_PLUGIN_FACTORY_WITH_JSON(PluginFactory, "networkplugin.json", registerPlugin<NetworkPlugin>();)
+
+#include "network.moc"
diff --git a/processplugins/network/network.h b/processplugins/network/network.h
new file mode 100644
index 0000000..89773e2
--- /dev/null
+++ b/processplugins/network/network.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <process_data_provider.h>
+
+class QProcess;
+
+class NetworkSensor;
+class ApplicationNetworkSensor;
+
+class NetworkPlugin : public KSysGuard::ProcessDataProvider
+{
+ Q_OBJECT
+public:
+ NetworkPlugin(QObject *parent, const QVariantList &args);
+
+ void handleEnabledChanged(bool enabled) override;
+private:
+ QProcess *m_process = nullptr;
+ KSysGuard::ProcessAttribute *m_inboundSensor = nullptr;
+ KSysGuard::ProcessAttribute *m_outboundSensor = nullptr;
+};
diff --git a/processplugins/network/networkplugin.json b/processplugins/network/networkplugin.json
new file mode 100644
index 0000000..17579a1
--- /dev/null
+++ b/processplugins/network/networkplugin.json
@@ -0,0 +1,5 @@
+{
+ "KPlugin": {
+ "Description": "Per-application network usage"
+ }
+}
--
2.22.0
davidedmundson edited the content of this paste. (Show Details)Tue, Aug 20, 10:48 AM
davidedmundson changed the title of this paste from untitled to Masterwork From Distant Lands.