diff --git a/CMakeLists.txt b/CMakeLists.txt index 12e06776..272e9c7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,68 +1,85 @@ cmake_minimum_required(VERSION 3.0) project(ksysguard) set(PROJECT_VERSION "5.16.80") set(KSYSGUARD_VERSION 4.98.0) set(KSYSGUARD_STRING_VERSION "${KSYSGUARD_VERSION}") set(QT_MIN_VERSION "5.12.0") set(KF5_MIN_VERSION "5.58.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) include(CheckIncludeFiles) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMAddTests) include(ECMInstallIcons) include(FeatureSummary) find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Widgets ) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Config CoreAddons DBusAddons DocTools I18n IconThemes Init ItemViews KIO NewStuff Notifications WindowSystem ) find_package(KF5 REQUIRED COMPONENTS SysGuard) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) add_definitions(-DQT_USE_QSTRINGBUILDER) #add_definitions(-DQT_NO_CAST_FROM_ASCII) #add_definitions(-DQT_NO_CAST_TO_ASCII) #add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x060000) find_package(Sensors) set_package_properties(Sensors PROPERTIES TYPE OPTIONAL PURPOSE "Allows to show sensor information") +find_package(libpcap) +set_package_properties( + libpcap PROPERTIES + TYPE RECOMMENDED + PURPOSE "libpcap is used for per-application network usage." +) + +if (libpcap_FOUND) + find_package(Libcap) + set_package_properties(Libcap PROPERTIES + TYPE OPTIONAL + PURPOSE "Needed for setting capabilities of the per-application network plugin." + ) +endif() + include_directories(${CMAKE_CURRENT_BINARY_DIR}) configure_file(config-workspace.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-workspace.h) add_subdirectory( gui ) add_subdirectory( doc ) add_subdirectory( pics ) add_subdirectory( example ) add_subdirectory( ksysguardd ) +add_subdirectory( plugins ) + feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/cmake/FindLibcap.cmake b/cmake/FindLibcap.cmake new file mode 100644 index 00000000..4a324462 --- /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, +# +# 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 00000000..24bf6c71 --- /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/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 00000000..f7468e6e --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,3 @@ +if (libpcap_FOUND) + add_subdirectory(process/network) +endif() diff --git a/plugins/process/network/CMakeLists.txt b/plugins/process/network/CMakeLists.txt new file mode 100644 index 00000000..d7a2e047 --- /dev/null +++ b/plugins/process/network/CMakeLists.txt @@ -0,0 +1,21 @@ + +include(ECMQtDeclareLoggingCategory) + +add_subdirectory(helper) + +set(networkplugin_SRCS + network.cpp +) + +ecm_qt_declare_logging_category(networkplugin_SRCS + HEADER networklogging.h + IDENTIFIER KSYSGUARD_PLUGIN_NETWORK + CATEGORY_NAME org.kde.ksysguard.plugin.network +) + +configure_file(networkconstants.h.in networkconstants.h @ONLY) + +add_library(ksysguard_plugin_network MODULE ${networkplugin_SRCS}) +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/plugins/process/network/README.md b/plugins/process/network/README.md new file mode 100644 index 00000000..d862f201 --- /dev/null +++ b/plugins/process/network/README.md @@ -0,0 +1,26 @@ +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. To ensure the helper has +`cap_net_raw`, run `setcap cap_net_raw+ep ksgrd_network_helper` as root. + +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 ksysguard. diff --git a/plugins/process/network/helper/Accumulator.cpp b/plugins/process/network/helper/Accumulator.cpp new file mode 100644 index 00000000..50defe7d --- /dev/null +++ b/plugins/process/network/helper/Accumulator.cpp @@ -0,0 +1,91 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Accumulator.h" + +#include "Capture.h" +#include "ConnectionMapping.h" +#include "Packet.h" + +using namespace std::chrono_literals; + +Accumulator::Accumulator(std::shared_ptr capture, std::shared_ptr mapping) +{ + m_capture = capture; + m_mapping = mapping; + + m_running = true; + m_thread = std::thread { &Accumulator::loop, this }; +} + +Accumulator::PidDataCounterHash Accumulator::data() +{ + auto tmp = m_data; + + auto toErase = std::vector{}; + for (auto &entry : m_data) { + if (entry.second.first == 0 && entry.second.second == 0) { + toErase.push_back(entry.first); + } else { + entry.second.first = 0; + entry.second.second = 0; + } + } + + std::for_each(toErase.cbegin(), toErase.cend(), [this](int pid) { m_data.erase(pid); }); + + return tmp; +} + +void Accumulator::stop() +{ + m_running = false; + if (m_thread.joinable()) { + m_thread.join(); + } +} + +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) +{ + auto itr = m_data.find(pid); + if (itr == m_data.end()) { + m_data.emplace(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/plugins/process/network/helper/Accumulator.h b/plugins/process/network/helper/Accumulator.h new file mode 100644 index 00000000..02c84ad4 --- /dev/null +++ b/plugins/process/network/helper/Accumulator.h @@ -0,0 +1,63 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef ACCUMULATOR_H +#define ACCUMULATOR_H + +#include +#include +#include +#include + +#include "TimeStamps.h" +#include "Packet.h" + +class Capture; +class ConnectionMapping; +class Packet; + +class Accumulator +{ + +public: + using InboundOutboundData = std::pair; + using PidDataCounterHash = std::unordered_map; + + Accumulator(std::shared_ptr capture, std::shared_ptr mapping); + + PidDataCounterHash data(); + + void stop(); + +private: + void addData(Packet::Direction direction, const Packet &packet, int pid); + void loop(); + + std::shared_ptr m_capture; + std::shared_ptr m_mapping; + + std::thread m_thread; + std::atomic_bool m_running; + + PidDataCounterHash m_data; +}; + +#endif // ACCUMULATOR_H diff --git a/plugins/process/network/helper/CMakeLists.txt b/plugins/process/network/helper/CMakeLists.txt new file mode 100644 index 00000000..4f44d54e --- /dev/null +++ b/plugins/process/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 ${PCAP_LIBRARY}) +kde_target_enable_exceptions(ksgrd_network_helper PUBLIC) +set_target_properties(ksgrd_network_helper PROPERTIES CXX_STANDARD 14 CXX_STANDARD_REQUIRED TRUE) + +# Why can't CMake fix this itself?' +target_link_libraries(ksgrd_network_helper pthread) + +install(TARGETS ksgrd_network_helper DESTINATION ${KDE_INSTALL_LIBEXECDIR}/ksysguard) + +if (Libcap_FOUND) + install( + CODE "execute_process( + COMMAND ${SETCAP_EXECUTABLE} + CAP_NET_RAW=+ep + \$ENV{DESTDIR}${KDE_INSTALL_FULL_LIBEXECDIR}/ksysguard/ksgrd_network_helper)" + ) +endif() diff --git a/plugins/process/network/helper/Capture.cpp b/plugins/process/network/helper/Capture.cpp new file mode 100644 index 00000000..054e8079 --- /dev/null +++ b/plugins/process/network/helper/Capture.cpp @@ -0,0 +1,172 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Capture.h" + +#include +#include + +#include + +#include "Packet.h" +#include "TimeStamps.h" + +using namespace std::string_literals; + +void pcapDispatchCallback(uint8_t *user, const struct pcap_pkthdr *h, const uint8_t *bytes) +{ + reinterpret_cast(user)->handlePacket(h, bytes); +} + +Capture::Capture(const std::string &interface) +{ + m_interface = interface; +} + +Capture::~Capture() +{ + if (m_pcap) { + if (m_active) { + stop(); + } + + pcap_close(m_pcap); + } +} + +bool Capture::start() +{ + auto device = m_interface.empty() ? (const char *)nullptr : m_interface.c_str(); + + char errorBuffer[PCAP_ERRBUF_SIZE]; + m_pcap = pcap_create(device, errorBuffer); + if (!m_pcap) { + m_error = std::string(errorBuffer, PCAP_ERRBUF_SIZE); + 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(); + } +} + +std::string 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 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(this)); +} + +bool Capture::checkError(int result) +{ + switch (result) { + case PCAP_ERROR_ACTIVATED: + m_error = "The handle has already been activated"s; + return true; + case PCAP_ERROR_NO_SUCH_DEVICE: + m_error = "The capture source specified when the handle was created doesn't exist"s; + return true; + case PCAP_ERROR_PERM_DENIED: + m_error = "The process doesn't have permission to open the capture source"s; + return true; + case PCAP_ERROR_PROMISC_PERM_DENIED: + m_error = "The process has permission to open the capture source but doesn't have permission to put it into promiscuous mode"s; + return true; + case PCAP_ERROR_RFMON_NOTSUP: + m_error = "Monitor mode was specified but the capture source doesn't support monitor mode"s; + return true; + case PCAP_ERROR_IFACE_NOT_UP: + m_error = "The capture source device is not up"s; + return true; + case PCAP_ERROR: + m_error = std::string(pcap_geterr(m_pcap)); + return true; + } + + return false; +} + +void Capture::handlePacket(const struct pcap_pkthdr *header, const uint8_t *data) +{ + auto timeStamp = std::chrono::time_point_cast(std::chrono::system_clock::from_time_t(header->ts.tv_sec) + std::chrono::microseconds { header->ts.tv_usec }); + + m_packetCount++; + { + std::lock_guard lock { m_mutex }; + m_queue.emplace_back(timeStamp, data, header->caplen, header->len); + } + + m_condition.notify_all(); +} diff --git a/plugins/process/network/helper/Capture.h b/plugins/process/network/helper/Capture.h new file mode 100644 index 00000000..6c133b3a --- /dev/null +++ b/plugins/process/network/helper/Capture.h @@ -0,0 +1,65 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CAPTURE_H +#define CAPTURE_H + +#include +#include +#include +#include +#include + +class pcap; +class Packet; + +class Capture +{ +public: + Capture(const std::string &interface = std::string{}); + ~Capture(); + + bool start(); + void stop(); + std::string lastError() const; + void reportStatistics(); + Packet nextPacket(); + + void handlePacket(const struct pcap_pkthdr *header, const uint8_t *data); + +private: + void loop(); + bool checkError(int result); + + std::string m_interface; + std::string m_error; + std::atomic_bool m_active; + std::thread m_thread; + std::mutex m_mutex; + std::condition_variable m_condition; + std::deque m_queue; + + int m_packetCount = 0; + + pcap *m_pcap; +}; + +#endif // CAPTURE_H diff --git a/plugins/process/network/helper/ConnectionMapping.cpp b/plugins/process/network/helper/ConnectionMapping.cpp new file mode 100644 index 00000000..e06e67e3 --- /dev/null +++ b/plugins/process/network/helper/ConnectionMapping.cpp @@ -0,0 +1,191 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ConnectionMapping.h" + +#include +#include + +#include +#include +#include + +using namespace std::string_literals; + +// Convert /proc/net/tcp's mangled big-endian notation to a host-endian int32' +uint32_t tcpToInt(const std::string &part) +{ + uint32_t result = 0; + result |= std::stoi(part.substr(0, 2), 0, 16) << 24; + result |= std::stoi(part.substr(2, 2), 0, 16) << 16; + result |= std::stoi(part.substr(4, 2), 0, 16) << 8; + result |= std::stoi(part.substr(6, 2), 0, 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. + std::regex("\\s*\\d+: (?:(\\w{8})|(\\w{32})):([A-F0-9]{4}) (.{94}|.{70}) (\\d+) .*", std::regex::ECMAScript | std::regex::optimize); + + parseProc(); +} + +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 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)) { + std::smatch match; + if (!std::regex_match(data, match, m_socketFileMatch)) { + continue; + } + + Packet::Address localAddress; + if (!match.str(1).empty()) { + localAddress.address[3] = tcpToInt(match.str(1)); + } else { + auto ipv6 = match.str(2); + if (ipv6.compare(0, 24, "0000000000000000FFFF0000")) { + // Some applications (like Steam) use ipv6 sockets with ipv4. + // This results in ipv4 addresses that end up in the tcp6 file. + // They seem to start with 0000000000000000FFFF0000, so if we + // detect that, assume it is ipv4-over-ipv6. + localAddress.address[3] = tcpToInt(ipv6.substr(24,8)); + } else { + localAddress.address[0] = tcpToInt(ipv6.substr(0, 8)); + localAddress.address[1] = tcpToInt(ipv6.substr(8, 8)); + localAddress.address[2] = tcpToInt(ipv6.substr(16, 8)); + localAddress.address[3] = tcpToInt(ipv6.substr(24, 8)); + } + } + + localAddress.port = std::stoi(match.str(3), 0, 16); + auto inode = std::stoi(match.str(5)); + m_localToINode.insert(std::make_pair(localAddress, inode)); + m_inodes.insert(inode); + } +} diff --git a/plugins/process/network/helper/ConnectionMapping.h b/plugins/process/network/helper/ConnectionMapping.h new file mode 100644 index 00000000..c1768ea2 --- /dev/null +++ b/plugins/process/network/helper/ConnectionMapping.h @@ -0,0 +1,64 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CONNECTIONMAPPING_H +#define CONNECTIONMAPPING_H + +#include +#include +#include + +// #include + +#include "Packet.h" + +/** + * @todo write docs + */ +class ConnectionMapping +{ +public: + struct PacketResult { + int pid = 0; + Packet::Direction direction; + }; + + ConnectionMapping(); + + void parseProc(); + + PacketResult pidForPacket(const Packet &packet); + +private: + bool parseSockets(); + void parsePid(); + void parseSocketFile(const char* fileName); + + std::unordered_map m_localToINode; + std::unordered_map m_inodeToPid; + std::unordered_set m_inodes; + std::unordered_set m_pids; + +// QRegularExpression m_socketFileMatch; + std::regex m_socketFileMatch; +}; + +#endif // CONNECTIONMAPPING_H diff --git a/plugins/process/network/helper/Packet.cpp b/plugins/process/network/helper/Packet.cpp new file mode 100644 index 00000000..0eda883d --- /dev/null +++ b/plugins/process/network/helper/Packet.cpp @@ -0,0 +1,148 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "Packet.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +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(data); + switch (ntohs(header->sll_protocol)) { + case ETH_P_IP: + m_networkProtocol = NetworkProtocolType::IPv4; + if (sizeof(sll_header) <= dataLength) { + parseIPv4(data + sizeof(sll_header)); + } + break; + case ETH_P_IPV6: + m_networkProtocol = NetworkProtocolType::IPv6; + if (sizeof(sll_header) <= dataLength) { + 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(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(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(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(data); + m_sourceAddress.port = ntohs(udpHeader->uh_sport); + m_destinationAddress.port = ntohs(udpHeader->uh_dport); + break; + } + default: + m_transportProtocol = TransportProtocolType::Unknown; + break; + } +} diff --git a/plugins/process/network/helper/Packet.h b/plugins/process/network/helper/Packet.h new file mode 100644 index 00000000..2719175c --- /dev/null +++ b/plugins/process/network/helper/Packet.h @@ -0,0 +1,110 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef PACKET_H +#define PACKET_H + +#include +#include +#include +#include + +#include "TimeStamps.h" + +class Packet +{ +public: + enum class NetworkProtocolType { + Unknown, + IPv4, + IPv6, + }; + + enum class TransportProtocolType { + Unknown, + Tcp, + Udp, + }; + + enum class Direction { + Inbound, + Outbound, + }; + + struct Address + { + std::array address = { 0 }; + uint32_t port = 0; + + inline bool operator==(const Address &other) const + { + return address == other.address + && port == other.port; + } + }; + + Packet(); + + Packet(const TimeStamp::MicroSeconds &timeStamp, const uint8_t *data, uint32_t dataLength, uint32_t packetSize); + + ~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; +}; + +namespace std { + template <> struct hash + { + using argument_type = Packet::Address; + using result_type = std::size_t; + inline result_type operator()(argument_type const& address) const noexcept { + return std::hash{}(address.address[0]) + ^ (std::hash{}(address.address[1]) << 1) + ^ (std::hash{}(address.address[2]) << 2) + ^ (std::hash{}(address.address[3]) << 3) + ^ (std::hash{}(address.port) << 4); + } + }; +} + +#endif // PACKET_H diff --git a/plugins/process/network/helper/TimeStamps.h b/plugins/process/network/helper/TimeStamps.h new file mode 100644 index 00000000..9f275361 --- /dev/null +++ b/plugins/process/network/helper/TimeStamps.h @@ -0,0 +1,34 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TIMESTAMPS_H +#define TIMESTAMPS_H + +#include + +// This is a helper header to simplify some of the std::chrono usages. +namespace TimeStamp +{ +using MicroSeconds = std::chrono::time_point; +using Seconds = std::chrono::time_point; +} + +#endif diff --git a/plugins/process/network/helper/main.cpp b/plugins/process/network/helper/main.cpp new file mode 100644 index 00000000..915dc48c --- /dev/null +++ b/plugins/process/network/helper/main.cpp @@ -0,0 +1,104 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include + +#include "Accumulator.h" +#include "Capture.h" +#include "ConnectionMapping.h" +#include "Packet.h" +#include "TimeStamps.h" + +static std::atomic_bool g_running{false}; + +int main(int argc, char **argv) +{ + static struct option long_options[] = { + {"help", 0, nullptr, 'h'}, + {"stats", 0, nullptr, 's'}, + {nullptr, 0, nullptr, 0} + }; + + auto statsRequested = false; + auto optionIndex = 0; + auto option = -1; + while ((option = getopt_long(argc, argv, "", long_options, &optionIndex)) != -1) { + switch (option) { + case 's': + statsRequested = true; + break; + default: + std::cerr << "Usage: " << argv[0] << " [options]\n"; + std::cerr << "This is a helper application for tracking per-process network usage.\n"; + std::cerr << "\n"; + std::cerr << "Options:\n"; + std::cerr << " --stats Print packet capture statistics.\n"; + std::cerr << " --help Display this help.\n"; + return 0; + } + } + + auto mapping = std::make_shared(); + + auto capture = std::make_shared(); + if (!capture->start()) { + std::cerr << capture->lastError() << std::endl; + return 1; + } + + auto accumulator = std::make_shared(capture, mapping); + + signal(SIGINT, [](int) { g_running = false; }); + signal(SIGTERM, [](int) { g_running = false; }); + + g_running = true; + while(g_running) { + auto data = accumulator->data(); + auto timeStamp = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + + if (statsRequested != 0) { + capture->reportStatistics(); + } + + if (data.empty()) { + std::cout << std::put_time(std::localtime(&timeStamp), "%T") << std::endl; + } else { + for (auto itr = data.begin(); itr != data.end(); ++itr) { + std::cout << std::put_time(std::localtime(&timeStamp), "%T"); + std::cout << "|PID|" << (*itr).first << "|IN|" << (*itr).second.first << "|OUT|" << (*itr).second.second; + std::cout << std::endl; + } + } + + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + + accumulator->stop(); + capture->stop(); + + return 0; +} diff --git a/plugins/process/network/network.cpp b/plugins/process/network/network.cpp new file mode 100644 index 00000000..243f18b5 --- /dev/null +++ b/plugins/process/network/network.cpp @@ -0,0 +1,113 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "network.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "networklogging.h" +#include "networkconstants.h" + +using namespace KSysGuard; + +NetworkPlugin::NetworkPlugin(QObject *parent, const QVariantList &args) + : ProcessDataProvider(parent, args) +{ + const auto executable = NetworkConstants::HelperLocation; + if (!QFile::exists(executable)) { + qCWarning(KSYSGUARD_PLUGIN_NETWORK) << "Could not find ksgrd_network_helper"; + return; + } + + qCDebug(KSYSGUARD_PLUGIN_NETWORK) << "Network plugin loading"; + qCDebug(KSYSGUARD_PLUGIN_NETWORK) << "Found helper at" << qPrintable(executable); + + m_inboundSensor = new ProcessAttribute(QStringLiteral("netInbound"), i18n("Download Speed"), this); + m_inboundSensor->setShortName(i18n("Download")); + m_inboundSensor->setUnit(KSysGuard::UnitByteRate); + m_outboundSensor = new ProcessAttribute(QStringLiteral("netOutbound"), i18n("Upload Speed"), this); + m_outboundSensor->setShortName(i18n("Upload")); + m_outboundSensor->setUnit(KSysGuard::UnitByteRate); + + addProcessAttribute(m_inboundSensor); + addProcessAttribute(m_outboundSensor); + + m_process = new QProcess(this); + m_process->setProgram(executable); + + connect(m_process, QOverload::of(&QProcess::finished), [=](int exitCode, QProcess::ExitStatus status) { + if (exitCode != 0 || status != QProcess::NormalExit) { + qCWarning(KSYSGUARD_PLUGIN_NETWORK) << "Helper process terminated abnormally!"; + qCWarning(KSYSGUARD_PLUGIN_NETWORK) << m_process->readAllStandardError(); + } + }); + + connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() { + while (m_process->canReadLine()) { + const QString line = QString::fromUtf8(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() < 7) { + continue; + } + + long pid = parts.at(2).toLong(); + + auto timeStamp = QDateTime::currentDateTimeUtc(); + timeStamp.setTime(QTime::fromString(parts.at(0).toString(), QStringLiteral("HH:mm:ss"))); + + auto bytesIn = parts.at(4).toUInt(); + auto bytesOut = parts.at(6).toUInt(); + + auto process = getProcess(pid); + if (!process) { + return; + } + + m_inboundSensor->setData(process, bytesIn); + m_outboundSensor->setData(process, bytesOut); + } + }); +} + +void NetworkPlugin::handleEnabledChanged(bool enabled) +{ + if (enabled) { + qCDebug(KSYSGUARD_PLUGIN_NETWORK) << "Network plugin enabled, starting helper"; + m_process->start(); + } else { + qCDebug(KSYSGUARD_PLUGIN_NETWORK) << "Network plugin disabled, stopping helper"; + m_process->terminate(); + } +} + +K_PLUGIN_FACTORY_WITH_JSON(PluginFactory, "networkplugin.json", registerPlugin();) + +#include "network.moc" diff --git a/plugins/process/network/network.h b/plugins/process/network/network.h new file mode 100644 index 00000000..d2af8c8b --- /dev/null +++ b/plugins/process/network/network.h @@ -0,0 +1,41 @@ +/* + * This file is part of KSysGuard. + * Copyright 2019 Arjen Hiemstra + * + * This program 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; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +class QProcess; + +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/plugins/process/network/networkconstants.h.in b/plugins/process/network/networkconstants.h.in new file mode 100644 index 00000000..f4556252 --- /dev/null +++ b/plugins/process/network/networkconstants.h.in @@ -0,0 +1,7 @@ +#pragma once + +namespace NetworkConstants { + +static const QString HelperLocation = QStringLiteral("@KDE_INSTALL_FULL_LIBEXECDIR@/ksysguard/ksgrd_network_helper"); + +} diff --git a/plugins/process/network/networkplugin.json b/plugins/process/network/networkplugin.json new file mode 100644 index 00000000..17579a11 --- /dev/null +++ b/plugins/process/network/networkplugin.json @@ -0,0 +1,5 @@ +{ + "KPlugin": { + "Description": "Per-application network usage" + } +}