/ test / functional / interface_usdt_net.py
interface_usdt_net.py
  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(pid=self.nodes[0].process.pid)
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, cflags=["-Wno-error=implicit-function-declaration"])
118  
119          EXPECTED_INOUTBOUND_VERSION_MSG = 1
120          checked_inbound_version_msg = 0
121          checked_outbound_version_msg = 0
122          events = []
123  
124          def check_p2p_message(event, is_inbound):
125              nonlocal checked_inbound_version_msg, checked_outbound_version_msg
126              if event.msg_type.decode("utf-8") == "version":
127                  self.log.info(
128                      f"check_p2p_message(): {'inbound' if is_inbound else 'outbound'} {event}")
129                  peer = self.nodes[0].getpeerinfo()[0]
130                  msg = msg_version()
131                  msg.deserialize(BytesIO(bytes(event.msg[:event.msg_size])))
132                  assert_equal(peer["id"], event.peer_id, peer["id"])
133                  assert_equal(peer["addr"], event.peer_addr.decode("utf-8"))
134                  assert_equal(peer["connection_type"],
135                               event.peer_conn_type.decode("utf-8"))
136                  if is_inbound:
137                      checked_inbound_version_msg += 1
138                  else:
139                      checked_outbound_version_msg += 1
140  
141          def handle_inbound(_, data, __):
142              event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents
143              events.append((event, True))
144  
145          def handle_outbound(_, data, __):
146              event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents
147              events.append((event, False))
148  
149          bpf["inbound_messages"].open_perf_buffer(handle_inbound)
150          bpf["outbound_messages"].open_perf_buffer(handle_outbound)
151  
152          self.log.info("connect a P2P test node to our bitcoind node")
153          test_node = P2PInterface()
154          self.nodes[0].add_p2p_connection(test_node)
155          bpf.perf_buffer_poll(timeout=200)
156  
157          self.log.info(
158              "check receipt and content of in- and outbound version messages")
159          for event, is_inbound in events:
160              check_p2p_message(event, is_inbound)
161          assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG,
162                       checked_inbound_version_msg)
163          assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG,
164                       checked_outbound_version_msg)
165  
166  
167          bpf.cleanup()
168  
169  
170  if __name__ == '__main__':
171      NetTracepointTest().main()