|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2022 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
| 5 | + |
| 6 | +""" Tests the net:* tracepoint API interface. |
| 7 | + See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-net |
| 8 | +""" |
| 9 | + |
| 10 | +import ctypes |
| 11 | +from io import BytesIO |
| 12 | +# Test will be skipped if we don't have bcc installed |
| 13 | +try: |
| 14 | + from bcc import BPF, USDT # type: ignore[import] |
| 15 | +except ImportError: |
| 16 | + pass |
| 17 | +from test_framework.messages import msg_version |
| 18 | +from test_framework.p2p import P2PInterface |
| 19 | +from test_framework.test_framework import BitcoinTestFramework |
| 20 | +from test_framework.util import assert_equal |
| 21 | + |
| 22 | +# Tor v3 addresses are 62 chars + 6 chars for the port (':12345'). |
| 23 | +MAX_PEER_ADDR_LENGTH = 68 |
| 24 | +MAX_PEER_CONN_TYPE_LENGTH = 20 |
| 25 | +MAX_MSG_TYPE_LENGTH = 20 |
| 26 | +# We won't process messages larger than 150 byte in this test. For reading |
| 27 | +# larger messanges see contrib/tracing/log_raw_p2p_msgs.py |
| 28 | +MAX_MSG_DATA_LENGTH = 150 |
| 29 | + |
| 30 | +net_tracepoints_program = """ |
| 31 | +#include <uapi/linux/ptrace.h> |
| 32 | +
|
| 33 | +#define MAX_PEER_ADDR_LENGTH {} |
| 34 | +#define MAX_PEER_CONN_TYPE_LENGTH {} |
| 35 | +#define MAX_MSG_TYPE_LENGTH {} |
| 36 | +#define MAX_MSG_DATA_LENGTH {} |
| 37 | +""".format( |
| 38 | + MAX_PEER_ADDR_LENGTH, |
| 39 | + MAX_PEER_CONN_TYPE_LENGTH, |
| 40 | + MAX_MSG_TYPE_LENGTH, |
| 41 | + MAX_MSG_DATA_LENGTH |
| 42 | +) + """ |
| 43 | +#define MIN(a,b) ({ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; }) |
| 44 | +
|
| 45 | +struct p2p_message |
| 46 | +{ |
| 47 | + u64 peer_id; |
| 48 | + char peer_addr[MAX_PEER_ADDR_LENGTH]; |
| 49 | + char peer_conn_type[MAX_PEER_CONN_TYPE_LENGTH]; |
| 50 | + char msg_type[MAX_MSG_TYPE_LENGTH]; |
| 51 | + u64 msg_size; |
| 52 | + u8 msg[MAX_MSG_DATA_LENGTH]; |
| 53 | +}; |
| 54 | +
|
| 55 | +BPF_PERF_OUTPUT(inbound_messages); |
| 56 | +int trace_inbound_message(struct pt_regs *ctx) { |
| 57 | + struct p2p_message msg = {}; |
| 58 | + bpf_usdt_readarg(1, ctx, &msg.peer_id); |
| 59 | + bpf_usdt_readarg_p(2, ctx, &msg.peer_addr, MAX_PEER_ADDR_LENGTH); |
| 60 | + bpf_usdt_readarg_p(3, ctx, &msg.peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH); |
| 61 | + bpf_usdt_readarg_p(4, ctx, &msg.msg_type, MAX_MSG_TYPE_LENGTH); |
| 62 | + bpf_usdt_readarg(5, ctx, &msg.msg_size); |
| 63 | + bpf_usdt_readarg_p(6, ctx, &msg.msg, MIN(msg.msg_size, MAX_MSG_DATA_LENGTH)); |
| 64 | + inbound_messages.perf_submit(ctx, &msg, sizeof(msg)); |
| 65 | + return 0; |
| 66 | +} |
| 67 | +
|
| 68 | +BPF_PERF_OUTPUT(outbound_messages); |
| 69 | +int trace_outbound_message(struct pt_regs *ctx) { |
| 70 | + struct p2p_message msg = {}; |
| 71 | + bpf_usdt_readarg(1, ctx, &msg.peer_id); |
| 72 | + bpf_usdt_readarg_p(2, ctx, &msg.peer_addr, MAX_PEER_ADDR_LENGTH); |
| 73 | + bpf_usdt_readarg_p(3, ctx, &msg.peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH); |
| 74 | + bpf_usdt_readarg_p(4, ctx, &msg.msg_type, MAX_MSG_TYPE_LENGTH); |
| 75 | + bpf_usdt_readarg(5, ctx, &msg.msg_size); |
| 76 | + bpf_usdt_readarg_p(6, ctx, &msg.msg, MIN(msg.msg_size, MAX_MSG_DATA_LENGTH)); |
| 77 | + outbound_messages.perf_submit(ctx, &msg, sizeof(msg)); |
| 78 | + return 0; |
| 79 | +}; |
| 80 | +""" |
| 81 | + |
| 82 | + |
| 83 | +class NetTracepointTest(BitcoinTestFramework): |
| 84 | + def set_test_params(self): |
| 85 | + self.num_nodes = 1 |
| 86 | + |
| 87 | + def skip_test_if_missing_module(self): |
| 88 | + self.skip_if_platform_not_linux() |
| 89 | + self.skip_if_no_bitcoind_tracepoints() |
| 90 | + self.skip_if_no_python_bcc() |
| 91 | + self.skip_if_no_bpf_permissions() |
| 92 | + |
| 93 | + def run_test(self): |
| 94 | + # Tests the net:inbound_message and net:outbound_message tracepoints |
| 95 | + # See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-net |
| 96 | + |
| 97 | + class P2PMessage(ctypes.Structure): |
| 98 | + _fields_ = [ |
| 99 | + ("peer_id", ctypes.c_uint64), |
| 100 | + ("peer_addr", ctypes.c_char * MAX_PEER_ADDR_LENGTH), |
| 101 | + ("peer_conn_type", ctypes.c_char * MAX_PEER_CONN_TYPE_LENGTH), |
| 102 | + ("msg_type", ctypes.c_char * MAX_MSG_TYPE_LENGTH), |
| 103 | + ("msg_size", ctypes.c_uint64), |
| 104 | + ("msg", ctypes.c_ubyte * MAX_MSG_DATA_LENGTH), |
| 105 | + ] |
| 106 | + |
| 107 | + def __repr__(self): |
| 108 | + return f"P2PMessage(peer={self.peer_id}, addr={self.peer_addr.decode('utf-8')}, conn_type={self.peer_conn_type.decode('utf-8')}, msg_type={self.msg_type.decode('utf-8')}, msg_size={self.msg_size})" |
| 109 | + |
| 110 | + self.log.info( |
| 111 | + "hook into the net:inbound_message and net:outbound_message tracepoints") |
| 112 | + ctx = USDT(path=str(self.options.bitcoind)) |
| 113 | + ctx.enable_probe(probe="net:inbound_message", |
| 114 | + fn_name="trace_inbound_message") |
| 115 | + ctx.enable_probe(probe="net:outbound_message", |
| 116 | + fn_name="trace_outbound_message") |
| 117 | + bpf = BPF(text=net_tracepoints_program, usdt_contexts=[ctx], debug=0) |
| 118 | + |
| 119 | + # The handle_* function is a ctypes callback function called from C. When |
| 120 | + # we assert in the handle_* function, the AssertError doesn't propagate |
| 121 | + # back to Python. The exception is ignored. We manually count and assert |
| 122 | + # that the handle_* functions succeeded. |
| 123 | + EXPECTED_INOUTBOUND_VERSION_MSG = 1 |
| 124 | + checked_inbound_version_msg = 0 |
| 125 | + checked_outbound_version_msg = 0 |
| 126 | + |
| 127 | + def check_p2p_message(event, inbound): |
| 128 | + nonlocal checked_inbound_version_msg, checked_outbound_version_msg |
| 129 | + if event.msg_type.decode("utf-8") == "version": |
| 130 | + self.log.info( |
| 131 | + f"check_p2p_message(): {'inbound' if inbound else 'outbound'} {event}") |
| 132 | + peer = self.nodes[0].getpeerinfo()[0] |
| 133 | + msg = msg_version() |
| 134 | + msg.deserialize(BytesIO(bytes(event.msg[:event.msg_size]))) |
| 135 | + assert_equal(peer["id"], event.peer_id, peer["id"]) |
| 136 | + assert_equal(peer["addr"], event.peer_addr.decode("utf-8")) |
| 137 | + assert_equal(peer["connection_type"], |
| 138 | + event.peer_conn_type.decode("utf-8")) |
| 139 | + if inbound: |
| 140 | + checked_inbound_version_msg += 1 |
| 141 | + else: |
| 142 | + checked_outbound_version_msg += 1 |
| 143 | + |
| 144 | + def handle_inbound(_, data, __): |
| 145 | + event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents |
| 146 | + check_p2p_message(event, True) |
| 147 | + |
| 148 | + def handle_outbound(_, data, __): |
| 149 | + event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents |
| 150 | + check_p2p_message(event, False) |
| 151 | + |
| 152 | + bpf["inbound_messages"].open_perf_buffer(handle_inbound) |
| 153 | + bpf["outbound_messages"].open_perf_buffer(handle_outbound) |
| 154 | + |
| 155 | + self.log.info("connect a P2P test node to our bitcoind node") |
| 156 | + test_node = P2PInterface() |
| 157 | + self.nodes[0].add_p2p_connection(test_node) |
| 158 | + bpf.perf_buffer_poll(timeout=200) |
| 159 | + |
| 160 | + self.log.info( |
| 161 | + "check that we got both an inbound and outbound version message") |
| 162 | + assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG, |
| 163 | + checked_inbound_version_msg) |
| 164 | + assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG, |
| 165 | + checked_outbound_version_msg) |
| 166 | + |
| 167 | + bpf.cleanup() |
| 168 | + |
| 169 | + |
| 170 | +if __name__ == '__main__': |
| 171 | + NetTracepointTest().main() |
0 commit comments