Skip to content

Commit 4224dec

Browse files
0xB10Cjb55
andcommitted
tracing: Tracepoints for in- and outbound P2P msgs
Can be used to monitor in- and outbound node traffic. Based on ealier work by jb55. Co-authored-by: William Casarin <[email protected]>
1 parent 469b71a commit 4224dec

File tree

7 files changed

+639
-1
lines changed

7 files changed

+639
-1
lines changed

Diff for: contrib/tracing/README.md

+120
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,123 @@ example, to point to release builds if needed. See the
4343
kernel VM. This means the bpftrace and BCC examples must be executed with root
4444
privileges. Make sure to carefully review any scripts that you run with root
4545
privileges first!**
46+
47+
### log_p2p_traffic.bt
48+
49+
A bpftrace script logging information about inbound and outbound P2P network
50+
messages. Based on the `net:inbound_message` and `net:outbound_message`
51+
tracepoints.
52+
53+
By default, `bpftrace` limits strings to 64 bytes due to the limited stack size
54+
in the eBPF VM. For example, Tor v3 addresses exceed the string size limit which
55+
results in the port being cut off during logging. The string size limit can be
56+
increased with the `BPFTRACE_STRLEN` environment variable (`BPFTRACE_STRLEN=70`
57+
works fine).
58+
59+
```
60+
$ bpftrace contrib/tracing/log_p2p_traffic.bt
61+
```
62+
63+
Output
64+
```
65+
outbound 'ping' msg to peer 11 (outbound-full-relay, [2a02:b10c:f747:1:ef:fake:ipv6:addr]:8333) with 8 bytes
66+
inbound 'pong' msg from peer 11 (outbound-full-relay, [2a02:b10c:f747:1:ef:fake:ipv6:addr]:8333) with 8 bytes
67+
inbound 'inv' msg from peer 16 (outbound-full-relay, XX.XX.XXX.121:8333) with 37 bytes
68+
outbound 'getdata' msg to peer 16 (outbound-full-relay, XX.XX.XXX.121:8333) with 37 bytes
69+
inbound 'tx' msg from peer 16 (outbound-full-relay, XX.XX.XXX.121:8333) with 222 bytes
70+
outbound 'inv' msg to peer 9 (outbound-full-relay, faketorv3addressa2ufa6odvoi3s77j4uegey0xb10csyfyve2t33curbyd.onion:8333) with 37 bytes
71+
outbound 'inv' msg to peer 7 (outbound-full-relay, XX.XX.XXX.242:8333) with 37 bytes
72+
73+
```
74+
75+
### p2p_monitor.py
76+
77+
A BCC Python script using curses for an interactive P2P message monitor. Based
78+
on the `net:inbound_message` and `net:outbound_message` tracepoints.
79+
80+
Inbound and outbound traffic is listed for each peer together with information
81+
about the connection. Peers can be selected individually to view recent P2P
82+
messages.
83+
84+
```
85+
$ python3 contrib/tracing/p2p_monitor.py ./src/bitcoind
86+
```
87+
88+
Lists selectable peers and traffic and connection information.
89+
```
90+
P2P Message Monitor
91+
Navigate with UP/DOWN or J/K and select a peer with ENTER or SPACE to see individual P2P messages
92+
93+
PEER OUTBOUND INBOUND TYPE ADDR
94+
0 46 398 byte 61 1407590 byte block-relay-only XX.XX.XXX.196:8333
95+
11 1156 253570 byte 3431 2394924 byte outbound-full-relay XXX.X.XX.179:8333
96+
13 3425 1809620 byte 1236 305458 byte inbound XXX.X.X.X:60380
97+
16 1046 241633 byte 1589 1199220 byte outbound-full-relay 4faketorv2pbfu7x.onion:8333
98+
19 577 181679 byte 390 148951 byte outbound-full-relay kfake4vctorjv2o2.onion:8333
99+
20 11 1248 byte 13 1283 byte block-relay-only [2600:fake:64d9:b10c:4436:aaaa:fe:bb]:8333
100+
21 11 1248 byte 13 1299 byte block-relay-only XX.XXX.X.155:8333
101+
22 5 103 byte 1 102 byte feeler XX.XX.XXX.173:8333
102+
23 11 1248 byte 12 1255 byte block-relay-only XX.XXX.XXX.220:8333
103+
24 3 103 byte 1 102 byte feeler XXX.XXX.XXX.64:8333
104+
105+
```
106+
107+
Showing recent P2P messages between our node and a selected peer.
108+
109+
```
110+
----------------------------------------------------------------------
111+
| PEER 16 (4faketorv2pbfu7x.onion:8333) |
112+
| OUR NODE outbound-full-relay PEER |
113+
| <--- sendcmpct (9 bytes) |
114+
| inv (37 byte) ---> |
115+
| <--- ping (8 bytes) |
116+
| pong (8 byte) ---> |
117+
| inv (37 byte) ---> |
118+
| <--- addr (31 bytes) |
119+
| inv (37 byte) ---> |
120+
| <--- getheaders (1029 bytes) |
121+
| headers (1 byte) ---> |
122+
| <--- feefilter (8 bytes) |
123+
| <--- pong (8 bytes) |
124+
| <--- headers (82 bytes) |
125+
| <--- addr (30003 bytes) |
126+
| inv (1261 byte) ---> |
127+
| … |
128+
129+
```
130+
131+
### log_raw_p2p_msgs.py
132+
133+
A BCC Python script showcasing eBPF and USDT limitations when passing data
134+
larger than about 32kb. Based on the `net:inbound_message` and
135+
`net:outbound_message` tracepoints.
136+
137+
Bitcoin P2P messages can be larger than 32kb (e.g. `tx`, `block`, ...). The
138+
eBPF VM's stack is limited to 512 bytes, and we can't allocate more than about
139+
32kb for a P2P message in the eBPF VM. The **message data is cut off** when the
140+
message is larger than MAX_MSG_DATA_LENGTH (see script). This can be detected
141+
in user-space by comparing the data length to the message length variable. The
142+
message is cut off when the data length is smaller than the message length.
143+
A warning is included with the printed message data.
144+
145+
Data is submitted to user-space (i.e. to this script) via a ring buffer. The
146+
throughput of the ring buffer is limited. Each p2p_message is about 32kb in
147+
size. In- or outbound messages submitted to the ring buffer in rapid
148+
succession fill the ring buffer faster than it can be read. Some messages are
149+
lost. BCC prints: `Possibly lost 2 samples` on lost messages.
150+
151+
152+
```
153+
$ python3 contrib/tracing/log_raw_p2p_msgs.py ./src/bitcoind
154+
```
155+
156+
```
157+
Logging raw P2P messages.
158+
Messages larger that about 32kb will be cut off!
159+
Some messages might be lost!
160+
outbound msg 'inv' from peer 4 (outbound-full-relay, XX.XXX.XX.4:8333) with 253 bytes: 0705000000be2245c8f844c9f763748e1a7…
161+
162+
Warning: incomplete message (only 32568 out of 53552 bytes)! inbound msg 'tx' from peer 32 (outbound-full-relay, XX.XXX.XXX.43:8333) with 53552 bytes: 020000000001fd3c01939c85ad6756ed9fc…
163+
164+
Possibly lost 2 samples
165+
```

Diff for: contrib/tracing/log_p2p_traffic.bt

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bpftrace
2+
3+
BEGIN
4+
{
5+
printf("Logging P2P traffic\n")
6+
}
7+
8+
usdt:./src/bitcoind:net:inbound_message
9+
{
10+
$peer_id = (int64) arg0;
11+
$peer_addr = str(arg1);
12+
$peer_type = str(arg2);
13+
$msg_type = str(arg3);
14+
$msg_len = arg4;
15+
printf("inbound '%s' msg from peer %d (%s, %s) with %d bytes\n", $msg_type, $peer_id, $peer_type, $peer_addr, $msg_len);
16+
}
17+
18+
usdt:./src/bitcoind:net:outbound_message
19+
{
20+
$peer_id = (int64) arg0;
21+
$peer_addr = str(arg1);
22+
$peer_type = str(arg2);
23+
$msg_type = str(arg3);
24+
$msg_len = arg4;
25+
26+
printf("outbound '%s' msg to peer %d (%s, %s) with %d bytes\n", $msg_type, $peer_id, $peer_type, $peer_addr, $msg_len);
27+
}
28+

Diff for: contrib/tracing/log_raw_p2p_msgs.py

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
#!/usr/bin/env python3
2+
3+
""" Demonstration of eBPF limitations and the effect on USDT with the
4+
net:inbound_message and net:outbound_message tracepoints. """
5+
6+
# This script shows a limitation of eBPF when data larger than 32kb is passed to
7+
# user-space. It uses BCC (https://github.com/iovisor/bcc) to load a sandboxed
8+
# eBPF program into the Linux kernel (root privileges are required). The eBPF
9+
# program attaches to two statically defined tracepoints. The tracepoint
10+
# 'net:inbound_message' is called when a new P2P message is received, and
11+
# 'net:outbound_message' is called on outbound P2P messages. The eBPF program
12+
# submits the P2P messages to this script via a BPF ring buffer. The submitted
13+
# messages are printed.
14+
15+
# eBPF Limitations:
16+
#
17+
# Bitcoin P2P messages can be larger than 32kb (e.g. tx, block, ...). The eBPF
18+
# VM's stack is limited to 512 bytes, and we can't allocate more than about 32kb
19+
# for a P2P message in the eBPF VM. The message data is cut off when the message
20+
# is larger than MAX_MSG_DATA_LENGTH (see definition below). This can be detected
21+
# in user-space by comparing the data length to the message length variable. The
22+
# message is cut off when the data length is smaller than the message length.
23+
# A warning is included with the printed message data.
24+
#
25+
# Data is submitted to user-space (i.e. to this script) via a ring buffer. The
26+
# throughput of the ring buffer is limited. Each p2p_message is about 32kb in
27+
# size. In- or outbound messages submitted to the ring buffer in rapid
28+
# succession fill the ring buffer faster than it can be read. Some messages are
29+
# lost.
30+
#
31+
# BCC prints: "Possibly lost 2 samples" on lost messages.
32+
33+
import sys
34+
from bcc import BPF, USDT
35+
36+
# BCC: The C program to be compiled to an eBPF program (by BCC) and loaded into
37+
# a sandboxed Linux kernel VM.
38+
program = """
39+
#include <uapi/linux/ptrace.h>
40+
41+
#define MIN(a,b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; })
42+
43+
// Maximum possible allocation size
44+
// from include/linux/percpu.h in the Linux kernel
45+
#define PCPU_MIN_UNIT_SIZE (32 << 10)
46+
47+
// Tor v3 addresses are 62 chars + 6 chars for the port (':12345').
48+
#define MAX_PEER_ADDR_LENGTH 62 + 6
49+
#define MAX_PEER_CONN_TYPE_LENGTH 20
50+
#define MAX_MSG_TYPE_LENGTH 20
51+
#define MAX_MSG_DATA_LENGTH PCPU_MIN_UNIT_SIZE - 200
52+
53+
struct p2p_message
54+
{
55+
u64 peer_id;
56+
char peer_addr[MAX_PEER_ADDR_LENGTH];
57+
char peer_conn_type[MAX_PEER_CONN_TYPE_LENGTH];
58+
char msg_type[MAX_MSG_TYPE_LENGTH];
59+
u64 msg_size;
60+
u8 msg[MAX_MSG_DATA_LENGTH];
61+
};
62+
63+
// We can't store the p2p_message struct on the eBPF stack as it is limited to
64+
// 512 bytes and P2P message can be bigger than 512 bytes. However, we can use
65+
// an BPF-array with a length of 1 to allocate up to 32768 bytes (this is
66+
// defined by PCPU_MIN_UNIT_SIZE in include/linux/percpu.h in the Linux kernel).
67+
// Also see https://github.com/iovisor/bcc/issues/2306
68+
BPF_ARRAY(msg_arr, struct p2p_message, 1);
69+
70+
// Two BPF perf buffers for pushing data (here P2P messages) to user-space.
71+
BPF_PERF_OUTPUT(inbound_messages);
72+
BPF_PERF_OUTPUT(outbound_messages);
73+
74+
int trace_inbound_message(struct pt_regs *ctx) {
75+
int idx = 0;
76+
struct p2p_message *msg = msg_arr.lookup(&idx);
77+
78+
// lookup() does not return a NULL pointer. However, the BPF verifier
79+
// requires an explicit check that that the `msg` pointer isn't a NULL
80+
// pointer. See https://github.com/iovisor/bcc/issues/2595
81+
if (msg == NULL) return 1;
82+
83+
bpf_usdt_readarg(1, ctx, &msg->peer_id);
84+
bpf_usdt_readarg_p(2, ctx, &msg->peer_addr, MAX_PEER_ADDR_LENGTH);
85+
bpf_usdt_readarg_p(3, ctx, &msg->peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH);
86+
bpf_usdt_readarg_p(4, ctx, &msg->msg_type, MAX_MSG_TYPE_LENGTH);
87+
bpf_usdt_readarg(5, ctx, &msg->msg_size);
88+
bpf_usdt_readarg_p(6, ctx, &msg->msg, MIN(msg->msg_size, MAX_MSG_DATA_LENGTH));
89+
90+
inbound_messages.perf_submit(ctx, msg, sizeof(*msg));
91+
return 0;
92+
};
93+
94+
int trace_outbound_message(struct pt_regs *ctx) {
95+
int idx = 0;
96+
struct p2p_message *msg = msg_arr.lookup(&idx);
97+
98+
// lookup() does not return a NULL pointer. However, the BPF verifier
99+
// requires an explicit check that that the `msg` pointer isn't a NULL
100+
// pointer. See https://github.com/iovisor/bcc/issues/2595
101+
if (msg == NULL) return 1;
102+
103+
bpf_usdt_readarg(1, ctx, &msg->peer_id);
104+
bpf_usdt_readarg_p(2, ctx, &msg->peer_addr, MAX_PEER_ADDR_LENGTH);
105+
bpf_usdt_readarg_p(3, ctx, &msg->peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH);
106+
bpf_usdt_readarg_p(4, ctx, &msg->msg_type, MAX_MSG_TYPE_LENGTH);
107+
bpf_usdt_readarg(5, ctx, &msg->msg_size);
108+
bpf_usdt_readarg_p(6, ctx, &msg->msg, MIN(msg->msg_size, MAX_MSG_DATA_LENGTH));
109+
110+
outbound_messages.perf_submit(ctx, msg, sizeof(*msg));
111+
return 0;
112+
};
113+
"""
114+
115+
116+
def print_message(event, inbound):
117+
print(f"%s %s msg '%s' from peer %d (%s, %s) with %d bytes: %s" %
118+
(
119+
f"Warning: incomplete message (only %d out of %d bytes)!" % (
120+
len(event.msg), event.msg_size) if len(event.msg) < event.msg_size else "",
121+
"inbound" if inbound else "outbound",
122+
event.msg_type.decode("utf-8"),
123+
event.peer_id,
124+
event.peer_conn_type.decode("utf-8"),
125+
event.peer_addr.decode("utf-8"),
126+
event.msg_size,
127+
bytes(event.msg[:event.msg_size]).hex(),
128+
)
129+
)
130+
131+
132+
def main(bitcoind_path):
133+
bitcoind_with_usdts = USDT(path=str(bitcoind_path))
134+
135+
# attaching the trace functions defined in the BPF program to the tracepoints
136+
bitcoind_with_usdts.enable_probe(
137+
probe="inbound_message", fn_name="trace_inbound_message")
138+
bitcoind_with_usdts.enable_probe(
139+
probe="outbound_message", fn_name="trace_outbound_message")
140+
bpf = BPF(text=program, usdt_contexts=[bitcoind_with_usdts])
141+
142+
# BCC: perf buffer handle function for inbound_messages
143+
def handle_inbound(_, data, size):
144+
""" Inbound message handler.
145+
146+
Called each time a message is submitted to the inbound_messages BPF table."""
147+
148+
event = bpf["inbound_messages"].event(data)
149+
print_message(event, True)
150+
151+
# BCC: perf buffer handle function for outbound_messages
152+
153+
def handle_outbound(_, data, size):
154+
""" Outbound message handler.
155+
156+
Called each time a message is submitted to the outbound_messages BPF table."""
157+
158+
event = bpf["outbound_messages"].event(data)
159+
print_message(event, False)
160+
161+
# BCC: add handlers to the inbound and outbound perf buffers
162+
bpf["inbound_messages"].open_perf_buffer(handle_inbound)
163+
bpf["outbound_messages"].open_perf_buffer(handle_outbound)
164+
165+
print("Logging raw P2P messages.")
166+
print("Messages larger that about 32kb will be cut off!")
167+
print("Some messages might be lost!")
168+
while True:
169+
try:
170+
bpf.perf_buffer_poll()
171+
except KeyboardInterrupt:
172+
exit()
173+
174+
175+
if __name__ == "__main__":
176+
if len(sys.argv) < 2:
177+
print("USAGE:", sys.argv[0], "path/to/bitcoind")
178+
exit()
179+
path = sys.argv[1]
180+
main(path)

0 commit comments

Comments
 (0)