From f73776fda75dd2ac34df2d0426bab3aba67983bd Mon Sep 17 00:00:00 2001 From: David Edmundson 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, +# +# 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 + +using namespace std::chrono_literals; + +Accumulator::Accumulator(std::shared_ptr capture, std::shared_ptr 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 +#include +#include + +#include "TimeStamps.h" +#include "Packet.h" + +#include +#include + +class Capture; +class ConnectionMapping; +class Packet; + +/** + * @todo write docs + */ +class Accumulator : public QObject +{ + Q_OBJECT + +public: + using InboundOutboundData = QPair; + using PidDataCounterHash = QHash; + + /** + * Constructor + * + * @param parent TODO + */ + Accumulator(std::shared_ptr capture, std::shared_ptr 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 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/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 + +#include + +#include "Packet.h" +#include "TimeStamps.h" + +void pcapDispatchCallback(uchar *user, const struct pcap_pkthdr *h, const uchar *bytes) +{ + reinterpret_cast(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 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 = 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(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/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 +#include +#include +#include + +#include + +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 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 +#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 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 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 +#include + +#include + +#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 m_localToINode; + std::unordered_map m_inodeToPid; + std::unordered_set m_inodes; + std::unordered_set 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 +#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; + 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(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; + } +} + +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 +#include +#include +#include + +#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 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 + { + 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/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 + +// 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 + +namespace TimeStamp +{ +using MicroSeconds = std::chrono::time_point; +using Seconds = std::chrono::time_point; +} + +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 +#include +#include + +#include +#include +#include +#include + +#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(); + + auto capture = std::make_shared(); + if (!capture->start()) { + qWarning() << capture->lastError(); + return 1; + } + + auto accumulator = std::make_shared(capture, mapping); + + TimeStamp::Seconds lastDisplayedStamp; + + QTimer timer; + timer.setInterval(1000); + QObject::connect(&timer, &QTimer::timeout, [&]() { + lastDisplayedStamp = std::chrono::time_point_cast(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 +#include +#include +#include +#include + +#include +#include + +#include + +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::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();) + +#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 + +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