Commit 1bb5880f authored by Arjen Hiemstra's avatar Arjen Hiemstra
Browse files

Add a per-process network information plugin

Summary:
This adds a ksysguard process plugin that provides information about
how much data is being used by a certain process. Since there is no
unpriviledged API available (yet?) for this information, a small
helper application is used that uses libpcap to capture packet sizes,
which is then mapped to individual processes by comparing sockets to
entries in /proc.

The helper application is intentionally kept small and should not
require anything else than `cap_net_raw`.

Depends on D23287

Test Plan:
Ensure ksgrd_network_helper has cap_net_raw set, by running
`setcap cap_net_raw+ep ksgrd_network_helper`. After that, start
ksysguard. The "upload" and "download" columns should be
automatically enabled and display network traffic for a certain
process.

Reviewers: #plasma, meven

Reviewed By: meven

Subscribers: meven, davidedmundson, plasma-devel

Tags: #plasma

Differential Revision: https://phabricator.kde.org/D23342
parent 5652a047
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)
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.
/*
* This file is part of KSysGuard.
* Copyright 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "Accumulator.h"
#include "Capture.h"
#include "ConnectionMapping.h"
#include "Packet.h"
using namespace std::chrono_literals;
Accumulator::Accumulator(std::shared_ptr<Capture> capture, std::shared_ptr<ConnectionMapping> 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<int>{};
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();
};
}
/*
* This file is part of KSysGuard.
* Copyright 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef ACCUMULATOR_H
#define ACCUMULATOR_H
#include <memory>
#include <thread>
#include <atomic>
#include <unordered_map>
#include "TimeStamps.h"
#include "Packet.h"
class Capture;
class ConnectionMapping;
class Packet;
class Accumulator
{
public:
using InboundOutboundData = std::pair<int, int>;
using PidDataCounterHash = std::unordered_map<int, InboundOutboundData>;
Accumulator(std::shared_ptr<Capture> capture, std::shared_ptr<ConnectionMapping> mapping);
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
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()
/*
* This file is part of KSysGuard.
* Copyright 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "Capture.h"
#include <string>
#include <iostream>
#include <pcap/pcap.h>
#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<Capture *>(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<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<uint8_t *>(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<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();
}
/*
* This file is part of KSysGuard.
* Copyright 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef CAPTURE_H
#define CAPTURE_H
#include <thread>
#include <mutex>
#include <condition_variable>
#include <deque>
#include <atomic>
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<Packet> m_queue;
int m_packetCount = 0;
pcap *m_pcap;
};
#endif // CAPTURE_H
/*
* This file is part of KSysGuard.
* Copyright 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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 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<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));