aboutsummaryrefslogtreecommitdiff
path: root/contrib/tracing/p2p_monitor.py
blob: 4ff701cac3c8c5d4e7b8a3f10a0f9b0a248ecc49 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
#!/usr/bin/env python3
# Copyright (c) 2021 The Bitcoin Core developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.

""" Interactive bitcoind P2P network traffic monitor utilizing USDT and the
    net:inbound_message and net:outbound_message tracepoints. """

# This script demonstrates what USDT for Bitcoin Core can enable. It uses BCC
# (https://github.com/iovisor/bcc) to load a sandboxed eBPF program into the
# Linux kernel (root privileges are required). The eBPF program attaches to two
# statically defined tracepoints. The tracepoint 'net:inbound_message' is called
# when a new P2P message is received, and 'net:outbound_message' is called on
# outbound P2P messages. The eBPF program submits the P2P messages to
# this script via a BPF ring buffer.

import sys
import curses
from curses import wrapper, panel
from bcc import BPF, USDT

# BCC: The C program to be compiled to an eBPF program (by BCC) and loaded into
# a sandboxed Linux kernel VM.
program = """
#include <uapi/linux/ptrace.h>

// Tor v3 addresses are 62 chars + 6 chars for the port (':12345').
// I2P addresses are 60 chars + 6 chars for the port (':12345').
#define MAX_PEER_ADDR_LENGTH 62 + 6
#define MAX_PEER_CONN_TYPE_LENGTH 20
#define MAX_MSG_TYPE_LENGTH 20

struct p2p_message
{
    u64     peer_id;
    char    peer_addr[MAX_PEER_ADDR_LENGTH];
    char    peer_conn_type[MAX_PEER_CONN_TYPE_LENGTH];
    char    msg_type[MAX_MSG_TYPE_LENGTH];
    u64     msg_size;
};


// Two BPF perf buffers for pushing data (here P2P messages) to user space.
BPF_PERF_OUTPUT(inbound_messages);
BPF_PERF_OUTPUT(outbound_messages);

int trace_inbound_message(struct pt_regs *ctx) {
    struct p2p_message msg = {};

    bpf_usdt_readarg(1, ctx, &msg.peer_id);
    bpf_usdt_readarg_p(2, ctx, &msg.peer_addr, MAX_PEER_ADDR_LENGTH);
    bpf_usdt_readarg_p(3, ctx, &msg.peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH);
    bpf_usdt_readarg_p(4, ctx, &msg.msg_type, MAX_MSG_TYPE_LENGTH);
    bpf_usdt_readarg(5, ctx, &msg.msg_size);

    inbound_messages.perf_submit(ctx, &msg, sizeof(msg));
    return 0;
};

int trace_outbound_message(struct pt_regs *ctx) {
    struct p2p_message msg = {};

    bpf_usdt_readarg(1, ctx, &msg.peer_id);
    bpf_usdt_readarg_p(2, ctx, &msg.peer_addr, MAX_PEER_ADDR_LENGTH);
    bpf_usdt_readarg_p(3, ctx, &msg.peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH);
    bpf_usdt_readarg_p(4, ctx, &msg.msg_type, MAX_MSG_TYPE_LENGTH);
    bpf_usdt_readarg(5, ctx, &msg.msg_size);

    outbound_messages.perf_submit(ctx, &msg, sizeof(msg));
    return 0;
};
"""


class Message:
    """ A P2P network message. """
    msg_type = ""
    size = 0
    data = bytes()
    inbound = False

    def __init__(self, msg_type, size, inbound):
        self.msg_type = msg_type
        self.size = size
        self.inbound = inbound


class Peer:
    """ A P2P network peer. """
    id = 0
    address = ""
    connection_type = ""
    last_messages = list()

    total_inbound_msgs = 0
    total_inbound_bytes = 0
    total_outbound_msgs = 0
    total_outbound_bytes = 0

    def __init__(self, id, address, connection_type):
        self.id = id
        self.address = address
        self.connection_type = connection_type
        self.last_messages = list()

    def add_message(self, message):
        self.last_messages.append(message)
        if len(self.last_messages) > 25:
            self.last_messages.pop(0)
        if message.inbound:
            self.total_inbound_bytes += message.size
            self.total_inbound_msgs += 1
        else:
            self.total_outbound_bytes += message.size
            self.total_outbound_msgs += 1


def main(bitcoind_path):
    peers = dict()

    bitcoind_with_usdts = USDT(path=str(bitcoind_path))

    # attaching the trace functions defined in the BPF program to the tracepoints
    bitcoind_with_usdts.enable_probe(
        probe="inbound_message", fn_name="trace_inbound_message")
    bitcoind_with_usdts.enable_probe(
        probe="outbound_message", fn_name="trace_outbound_message")
    bpf = BPF(text=program, usdt_contexts=[bitcoind_with_usdts])

    # BCC: perf buffer handle function for inbound_messages
    def handle_inbound(_, data, size):
        """ Inbound message handler.

        Called each time a message is submitted to the inbound_messages BPF table."""
        event = bpf["inbound_messages"].event(data)
        if event.peer_id not in peers:
            peer = Peer(event.peer_id, event.peer_addr.decode(
                "utf-8"), event.peer_conn_type.decode("utf-8"))
            peers[peer.id] = peer
        peers[event.peer_id].add_message(
            Message(event.msg_type.decode("utf-8"), event.msg_size, True))

    # BCC: perf buffer handle function for outbound_messages
    def handle_outbound(_, data, size):
        """ Outbound message handler.

        Called each time a message is submitted to the outbound_messages BPF table."""
        event = bpf["outbound_messages"].event(data)
        if event.peer_id not in peers:
            peer = Peer(event.peer_id, event.peer_addr.decode(
                "utf-8"), event.peer_conn_type.decode("utf-8"))
            peers[peer.id] = peer
        peers[event.peer_id].add_message(
            Message(event.msg_type.decode("utf-8"), event.msg_size, False))

    # BCC: add handlers to the inbound and outbound perf buffers
    bpf["inbound_messages"].open_perf_buffer(handle_inbound)
    bpf["outbound_messages"].open_perf_buffer(handle_outbound)

    wrapper(loop, bpf, peers)


def loop(screen, bpf, peers):
    screen.nodelay(1)
    cur_list_pos = 0
    win = curses.newwin(30, 70, 2, 7)
    win.erase()
    win.border(ord("|"), ord("|"), ord("-"), ord("-"),
               ord("-"), ord("-"), ord("-"), ord("-"))
    info_panel = panel.new_panel(win)
    info_panel.hide()

    ROWS_AVALIABLE_FOR_LIST = curses.LINES - 5
    scroll = 0

    while True:
        try:
            # BCC: poll the perf buffers for new events or timeout after 50ms
            bpf.perf_buffer_poll(timeout=50)

            ch = screen.getch()
            if (ch == curses.KEY_DOWN or ch == ord("j")) and cur_list_pos < len(
                    peers.keys()) -1 and info_panel.hidden():
                cur_list_pos += 1
                if cur_list_pos >= ROWS_AVALIABLE_FOR_LIST:
                    scroll += 1
            if (ch == curses.KEY_UP or ch == ord("k")) and cur_list_pos > 0 and info_panel.hidden():
                cur_list_pos -= 1
                if scroll > 0:
                    scroll -= 1
            if ch == ord('\n') or ch == ord(' '):
                if info_panel.hidden():
                    info_panel.show()
                else:
                    info_panel.hide()
            screen.erase()
            render(screen, peers, cur_list_pos, scroll, ROWS_AVALIABLE_FOR_LIST, info_panel)
            curses.panel.update_panels()
            screen.refresh()
        except KeyboardInterrupt:
            exit()


def render(screen, peers, cur_list_pos, scroll, ROWS_AVALIABLE_FOR_LIST, info_panel):
    """ renders the list of peers and details panel

    This code is unrelated to USDT, BCC and BPF.
    """
    header_format = "%6s  %-20s  %-20s  %-22s  %-67s"
    row_format = "%6s  %-5d %9d byte  %-5d %9d byte  %-22s  %-67s"

    screen.addstr(0, 1, (" P2P Message Monitor "), curses.A_REVERSE)
    screen.addstr(
        1, 0, (" Navigate with UP/DOWN or J/K and select a peer with ENTER or SPACE to see individual P2P messages"), curses.A_NORMAL)
    screen.addstr(3, 0,
                  header_format % ("PEER", "OUTBOUND", "INBOUND", "TYPE", "ADDR"), curses.A_BOLD | curses.A_UNDERLINE)
    peer_list = sorted(peers.keys())[scroll:ROWS_AVALIABLE_FOR_LIST+scroll]
    for i, peer_id in enumerate(peer_list):
        peer = peers[peer_id]
        screen.addstr(i + 4, 0,
                      row_format % (peer.id, peer.total_outbound_msgs, peer.total_outbound_bytes,
                                    peer.total_inbound_msgs, peer.total_inbound_bytes,
                                    peer.connection_type, peer.address),
                      curses.A_REVERSE if i + scroll == cur_list_pos else curses.A_NORMAL)
        if i + scroll == cur_list_pos:
            info_window = info_panel.window()
            info_window.erase()
            info_window.border(
                ord("|"), ord("|"), ord("-"), ord("-"),
                ord("-"), ord("-"), ord("-"), ord("-"))

            info_window.addstr(
                1, 1, f"PEER {peer.id} ({peer.address})".center(68), curses.A_REVERSE | curses.A_BOLD)
            info_window.addstr(
                2, 1, f" OUR NODE{peer.connection_type:^54}PEER ",
                curses.A_BOLD)
            for i, msg in enumerate(peer.last_messages):
                if msg.inbound:
                    info_window.addstr(
                        i + 3, 1, "%68s" %
                        (f"<--- {msg.msg_type} ({msg.size} bytes) "), curses.A_NORMAL)
                else:
                    info_window.addstr(
                        i + 3, 1, " %s (%d byte) --->" %
                        (msg.msg_type, msg.size), curses.A_NORMAL)


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("USAGE:", sys.argv[0], "path/to/bitcoind")
        exit()
    path = sys.argv[1]
    main(path)