ConnectionMapping.cpp 7.71 KB
Newer Older
1
/*
2
3
4
5
6
    SPDX-FileCopyrightText: 2019 Arjen Hiemstra <ahiemstra@heimr.nl>
    SPDX-FileCopyrightText: 2020 David Redondo <kde@david-redondo.de>

    SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
7
8
9

#include "ConnectionMapping.h"

Alexander Lohnau's avatar
Alexander Lohnau committed
10
#include <charconv>
11
12
13
14
15
16
17
#include <fstream>
#include <iostream>

#include <dirent.h>
#include <errno.h>
#include <unistd.h>

18
19
20
21
22
23
24
#include <arpa/inet.h>
#include <linux/inet_diag.h>
#include <linux/sock_diag.h>

#include <netlink/msg.h>
#include <netlink/netlink.h>

25
26
using namespace std::string_literals;

Alexander Lohnau's avatar
Alexander Lohnau committed
27
template<typename Key, typename Value>
28
inline void cleanupOldEntries(const std::unordered_set<Key> &keys, std::unordered_map<Key, Value> &map)
29
{
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
    for (auto itr = map.begin(); itr != map.end();) {
        if (keys.find(itr->first) == keys.end()) {
            itr = map.erase(itr);
        } else {
            itr++;
        }
    }
}

ConnectionMapping::inode_t toInode(const std::string_view &view)
{
    ConnectionMapping::inode_t value;
    if (auto status = std::from_chars(view.data(), view.data() + view.length(), value); status.ec == std::errc()) {
        return value;
    }
    return std::numeric_limits<ConnectionMapping::inode_t>::max();
46
47
}

Alexander Lohnau's avatar
Alexander Lohnau committed
48
int parseInetDiagMesg(struct nl_msg *msg, void *arg)
49
{
Alexander Lohnau's avatar
Alexander Lohnau committed
50
    auto self = static_cast<ConnectionMapping *>(arg);
51
    struct nlmsghdr *nlh = nlmsg_hdr(msg);
Alexander Lohnau's avatar
Alexander Lohnau committed
52
    auto inetDiagMsg = static_cast<inet_diag_msg *>(nlmsg_data(nlh));
53
54
55
56
57
    Packet::Address localAddress;
    if (inetDiagMsg->idiag_family == AF_INET) {
        // I expected to need ntohl calls here and bewlow for src but comparing to values gathered
        // by parsing proc they are not needed and even produce wrong results
        localAddress.address[3] = inetDiagMsg->id.idiag_src[0];
Alexander Lohnau's avatar
Alexander Lohnau committed
58
59
    } else if (inetDiagMsg->id.idiag_src[0] == 0 && inetDiagMsg->id.idiag_src[1] == 0 && inetDiagMsg->id.idiag_src[2] == 0xffff0000) {
        // Some applications (like Steam) use ipv6 sockets with ipv4.
60
61
62
63
64
65
66
67
68
        // 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] = inetDiagMsg->id.idiag_src[3];

    } else {
        std::memcpy(localAddress.address.data(), inetDiagMsg->id.idiag_src, sizeof(localAddress.address));
    }
    localAddress.port = ntohs(inetDiagMsg->id.idiag_sport);
69
70
71
72
73
74
75
76
77
78
79

    if (self->m_newState.addressToInode.find(localAddress) == self->m_newState.addressToInode.end()) {
        // new localAddress is found for which no socket inode is known
        // will trigger pid parsing
        self->m_newState.addressToInode.emplace(localAddress, inetDiagMsg->idiag_inode);
        self->m_newInode = true;
    }

    self->m_seenAddresses.insert(localAddress);
    self->m_seenInodes.insert(inetDiagMsg->idiag_inode);

80
81
82
    return NL_OK;
}

83
84
ConnectionMapping::ConnectionMapping()
{
85
    m_thread = std::thread(&ConnectionMapping::loop, this);
86
87
88
89
}

ConnectionMapping::PacketResult ConnectionMapping::pidForPacket(const Packet &packet)
{
90
    std::lock_guard<std::mutex> lock{m_mutex};
91

92
    PacketResult result;
93

94
95
    auto sourceInode = m_oldState.addressToInode.find(packet.sourceAddress());
    auto destInode = m_oldState.addressToInode.find(packet.destinationAddress());
96

97
98
    if (sourceInode == m_oldState.addressToInode.end() && destInode == m_oldState.addressToInode.end()) {
        return result;
99
100
    }

101
102
    auto inode = m_oldState.addressToInode.end();
    if (sourceInode != m_oldState.addressToInode.end()) {
103
104
105
106
107
108
109
        result.direction = Packet::Direction::Outbound;
        inode = sourceInode;
    } else {
        result.direction = Packet::Direction::Inbound;
        inode = destInode;
    }

110
111
    auto pid = m_oldState.inodeToPid.find((*inode).second);
    if (pid == m_oldState.inodeToPid.end()) {
112
113
114
115
116
117
118
        result.pid = -1;
    } else {
        result.pid = (*pid).second;
    }
    return result;
}

119
void ConnectionMapping::stop()
120
{
121
122
123
    m_running = false;
    if (m_thread.joinable()) {
        m_thread.join();
124
    }
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
}

void ConnectionMapping::loop()
{
    std::unique_ptr<nl_sock, decltype(&nl_socket_free)> socket{nl_socket_alloc(), nl_socket_free};

    nl_connect(socket.get(), NETLINK_SOCK_DIAG);
    nl_socket_modify_cb(socket.get(), NL_CB_VALID, NL_CB_CUSTOM, &parseInetDiagMesg, this);

    m_running = true;

    while (m_running) {
        m_seenAddresses.clear();
        m_seenInodes.clear();

        dumpSockets(socket.get());

        if (m_newInode) {
            parsePid();
            m_newInode = false;
        }

        cleanupOldEntries(m_seenAddresses, m_newState.addressToInode);
        cleanupOldEntries(m_seenInodes, m_newState.inodeToPid);

        {
            std::lock_guard<std::mutex> lock{m_mutex};
            m_oldState = m_newState;
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(500));
156
    }
157
158
}

159
bool ConnectionMapping::dumpSockets(nl_sock *socket)
160
{
161
162
    for (auto family : {AF_INET, AF_INET6}) {
        for (auto protocol : {IPPROTO_TCP, IPPROTO_UDP}) {
163
            if (!dumpSockets(socket, family, protocol)) {
164
165
166
167
168
169
                return false;
            }
        }
    }
    return true;
}
170

171
bool ConnectionMapping::dumpSockets(nl_sock *socket, int inet_family, int protocol)
172
173
174
175
176
177
{
    inet_diag_req_v2 inet_request;
    inet_request.id = {};
    inet_request.sdiag_family = inet_family;
    inet_request.sdiag_protocol = protocol;
    inet_request.idiag_states = -1;
178
    if (nl_send_simple(socket, SOCK_DIAG_BY_FAMILY, NLM_F_DUMP | NLM_F_REQUEST, &inet_request, sizeof(inet_diag_req_v2)) < 0) {
179
180
        return false;
    }
181
    if (nl_recvmsgs_default(socket) != 0) {
182
183
184
185
186
        return false;
    }
    return true;
}

187
188
189
void ConnectionMapping::parsePid()
{
    auto dir = opendir("/proc");
190
191
192
193
194
195
196
197
198

    std::array<char, 100> buffer;

    auto fdPath = "/proc/%/fd"s;
    // Ensure the string has enough space to accomodate large PIDs
    fdPath.reserve(30);

    // The only way to get a list of PIDs is to list the contents of /proc.
    // Any directory with a numeric name corresponds to a process and its PID.
199
    dirent *entry = nullptr;
200
201
202
203
204
205
206
207
208
209
210
211
212
    while ((entry = readdir(dir))) {
        if (entry->d_type != DT_DIR) {
            continue;
        }

        if (entry->d_name[0] < '0' || entry->d_name[0] > '9') {
            continue;
        }

        // We need to list the contents of a subdirectory of the PID directory.
        // To avoid multiple allocations we reserve the string above and reuse
        // it here.
        fdPath.replace(6, fdPath.find_last_of('/') - 6, entry->d_name);
213

214
215
        auto fdDir = opendir(fdPath.data());
        if (fdDir == NULL) {
216
217
218
219
            continue;
        }

        dirent *fd = nullptr;
220
221
        while ((fd = readdir(fdDir))) {
            if (fd->d_type != DT_LNK) {
222
                continue;
223
            }
224

225
226
227
            // /proc/PID/fd contains symlinks for each open fd in the process.
            // The symlink target contains information about what the fd is about.
            auto size = readlinkat(dirfd(fdDir), fd->d_name, buffer.data(), 99);
228
            buffer[size] = '\0';
229

230
            auto view = std::string_view(buffer.data(), 100);
231

232
233
234
235
236
237
            // In this case, we are only interested in sockets, for which the
            // symlink target starts with 'socket:', followed by the inode
            // number in square brackets.
            if (view.compare(0, 7, "socket:") != 0) {
                continue;
            }
238

239
240
241
242
243
            // Strip off the leading "socket:" part and the opening bracket,
            // then convert that to an inode number.
            auto inode = toInode(view.substr(8));
            if (inode != std::numeric_limits<inode_t>::max()) {
                m_newState.inodeToPid[inode] = std::stoi(entry->d_name);
244
245
246
            }
        }

247
        closedir(fdDir);
248
    }
249
250

    closedir(dir);
251
}