blob: 5f2a8062bdff3470264b58333869e01665a63e63 [file] [log] [blame]
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -05001#!/usr/bin/python
2# @lint-avoid-python-3-compatibility-imports
3#
4# tcpsubnet Summarize TCP bytes sent to different subnets.
5# For Linux, uses BCC, eBPF. Embedded C.
6#
Marko Myllynen27e7aea2018-09-26 20:09:07 +03007# USAGE: tcpsubnet [-h] [-v] [-J] [-f FORMAT] [-i INTERVAL] [subnets]
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -05008#
9# This uses dynamic tracing of kernel functions, and will need to be updated
10# to match kernel changes.
11#
12# This is an adaptation of tcptop from written by Brendan Gregg.
13#
14# WARNING: This traces all send at the TCP level, and while it
15# summarizes data in-kernel to reduce overhead, there may still be some
16# overhead at high TCP send/receive rates (eg, ~13% of one CPU at 100k TCP
17# events/sec. This is not the same as packet rate: funccount can be used to
18# count the kprobes below to find out the TCP rate). Test in a lab environment
19# first. If your send rate is low (eg, <1k/sec) then the overhead is
20# expected to be negligible.
21#
22# Copyright 2017 Rodrigo Manyari
23# Licensed under the Apache License, Version 2.0 (the "License")
24#
25# 03-Oct-2017 Rodrigo Manyari Created this based on tcptop.
26# 13-Feb-2018 Rodrigo Manyari Fix pep8 errors, some refactoring.
Rodrigo Manyariefcb30f2018-03-05 22:55:17 -050027# 05-Mar-2018 Rodrigo Manyari Add date time to output.
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -050028
29import argparse
30import json
31import logging
32import struct
33import socket
34from bcc import BPF
Rodrigo Manyariefcb30f2018-03-05 22:55:17 -050035from datetime import datetime as dt
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -050036from time import sleep
37
38# arguments
39examples = """examples:
40 ./tcpsubnet # Trace TCP sent to the default subnets:
41 # 127.0.0.1/32,10.0.0.0/8,172.16.0.0/12,
Rodrigo Manyariefcb30f2018-03-05 22:55:17 -050042 # 192.168.0.0/16,0.0.0.0/0
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -050043 ./tcpsubnet -f K # Trace TCP sent to the default subnets
44 # aggregated in KBytes.
45 ./tcpsubnet 10.80.0.0/24 # Trace TCP sent to 10.80.0.0/24 only
46 ./tcpsubnet -J # Format the output in JSON.
47"""
48
Rodrigo Manyariefcb30f2018-03-05 22:55:17 -050049default_subnets = "127.0.0.1/32,10.0.0.0/8," \
50 "172.16.0.0/12,192.168.0.0/16,0.0.0.0/0"
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -050051
52parser = argparse.ArgumentParser(
53 description="Summarize TCP send and aggregate by subnet",
54 formatter_class=argparse.RawDescriptionHelpFormatter,
55 epilog=examples)
56parser.add_argument("subnets", help="comma separated list of subnets",
57 type=str, nargs="?", default=default_subnets)
58parser.add_argument("-v", "--verbose", action="store_true",
59 help="output debug statements")
60parser.add_argument("-J", "--json", action="store_true",
61 help="format output in JSON")
62parser.add_argument("--ebpf", action="store_true",
63 help=argparse.SUPPRESS)
64parser.add_argument("-f", "--format", default="B",
65 help="[bkmBKM] format to report: bits, Kbits, Mbits, bytes, " +
Rodrigo Manyari14e23ad2018-03-02 20:41:42 -050066 "KBytes, MBytes (default B)", choices=["b", "k", "m", "B", "K", "M"])
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -050067parser.add_argument("-i", "--interval", default=1, type=int,
68 help="output interval, in seconds (default 1)")
69args = parser.parse_args()
70
71level = logging.INFO
72if args.verbose:
73 level = logging.DEBUG
74
75logging.basicConfig(level=level)
76
77logging.debug("Starting with the following args:")
78logging.debug(args)
79
80# args checking
81if int(args.interval) <= 0:
82 logging.error("Invalid interval, must be > 0. Exiting.")
83 exit(1)
84else:
85 args.interval = int(args.interval)
86
87# map of supported formats
88formats = {
89 "b": lambda x: (x * 8),
90 "k": lambda x: ((x * 8) / 1024),
91 "m": lambda x: ((x * 8) / pow(1024, 2)),
92 "B": lambda x: x,
93 "K": lambda x: x / 1024,
94 "M": lambda x: x / pow(1024, 2)
95}
96
97# Let's swap the string with the actual numeric value
98# once here so we don't have to do it on every interval
99formatFn = formats[args.format]
100
101# define the basic structure of the BPF program
102bpf_text = """
103#include <uapi/linux/ptrace.h>
104#include <net/sock.h>
105#include <bcc/proto.h>
106
107struct index_key_t {
108 u32 index;
109};
110
111BPF_HASH(ipv4_send_bytes, struct index_key_t);
112
113int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk,
114 struct msghdr *msg, size_t size)
115{
116 u16 family = sk->__sk_common.skc_family;
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -0500117
118 if (family == AF_INET) {
119 u32 dst = sk->__sk_common.skc_daddr;
120 unsigned categorized = 0;
121 __SUBNETS__
122 }
123 return 0;
124}
125"""
126
127
128# Takes in a mask and returns the integer equivalent
129# e.g.
130# mask_to_int(8) returns 4278190080
131def mask_to_int(n):
132 return ((1 << n) - 1) << (32 - n)
133
134# Takes in a list of subnets and returns a list
135# of tuple-3 containing:
136# - The subnet info at index 0
137# - The addr portion as an int at index 1
138# - The mask portion as an int at index 2
139#
140# e.g.
141# parse_subnets([10.10.0.0/24]) returns
142# [
143# ['10.10.0.0/24', 168427520, 4294967040],
144# ]
145def parse_subnets(subnets):
146 m = []
147 for s in subnets:
148 parts = s.split("/")
149 if len(parts) != 2:
150 msg = "Subnet [%s] is invalid, please refer to the examples." % s
151 raise ValueError(msg)
152 netaddr_int = 0
153 mask_int = 0
154 try:
155 netaddr_int = struct.unpack("!I", socket.inet_aton(parts[0]))[0]
156 except:
157 msg = ("Invalid net address in subnet [%s], " +
158 "please refer to the examples.") % s
159 raise ValueError(msg)
160 try:
161 mask_int = int(parts[1])
162 except:
163 msg = "Invalid mask in subnet [%s]. Mask must be an int" % s
164 raise ValueError(msg)
165 if mask_int < 0 or mask_int > 32:
166 msg = ("Invalid mask in subnet [%s]. Must be an " +
167 "int between 0 and 32.") % s
168 raise ValueError(msg)
169 mask_int = mask_to_int(int(parts[1]))
170 m.append([s, netaddr_int, mask_int])
171 return m
172
173def generate_bpf_subnets(subnets):
174 template = """
175 if (!categorized && (__NET_ADDR__ & __NET_MASK__) ==
176 (dst & __NET_MASK__)) {
177 struct index_key_t key = {.index = __POS__};
Javier Honduvilla Coto64bf9652018-08-01 06:50:19 +0200178 ipv4_send_bytes.increment(key, size);
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -0500179 categorized = 1;
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -0500180 }
181 """
182 bpf = ''
183 for i, s in enumerate(subnets):
184 branch = template
185 branch = branch.replace("__NET_ADDR__", str(socket.htonl(s[1])))
186 branch = branch.replace("__NET_MASK__", str(socket.htonl(s[2])))
187 branch = branch.replace("__POS__", str(i))
188 bpf += branch
189 return bpf
190
191subnets = []
192if args.subnets:
193 subnets = args.subnets.split(",")
194
195subnets = parse_subnets(subnets)
196
197logging.debug("Packets are going to be categorized in the following subnets:")
198logging.debug(subnets)
199
200bpf_subnets = generate_bpf_subnets(subnets)
201
202# initialize BPF
203bpf_text = bpf_text.replace("__SUBNETS__", bpf_subnets)
204
205logging.debug("Done preprocessing the BPF program, " +
206 "this is what will actually get executed:")
207logging.debug(bpf_text)
208
209if args.ebpf:
210 print(bpf_text)
211 exit()
212
213b = BPF(text=bpf_text)
214
215ipv4_send_bytes = b["ipv4_send_bytes"]
216
Rodrigo Manyari14e23ad2018-03-02 20:41:42 -0500217if not args.json:
218 print("Tracing... Output every %d secs. Hit Ctrl-C to end" % args.interval)
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -0500219
220# output
221exiting = 0
222while (1):
223
224 try:
225 sleep(args.interval)
226 except KeyboardInterrupt:
227 exiting = 1
228
229 # IPv4: build dict of all seen keys
230 keys = ipv4_send_bytes
231 for k, v in ipv4_send_bytes.items():
232 if k not in keys:
233 keys[k] = v
234
235 # to hold json data
236 data = {}
237
238 # output
Rodrigo Manyariefcb30f2018-03-05 22:55:17 -0500239 now = dt.now()
240 data['date'] = now.strftime('%x')
241 data['time'] = now.strftime('%X')
242 data['entries'] = {}
243 if not args.json:
244 print(now.strftime('[%x %X]'))
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -0500245 for k, v in reversed(sorted(keys.items(), key=lambda keys: keys[1].value)):
246 send_bytes = 0
247 if k in ipv4_send_bytes:
248 send_bytes = int(ipv4_send_bytes[k].value)
249 subnet = subnets[k.index][0]
250 send = formatFn(send_bytes)
251 if args.json:
Rodrigo Manyariefcb30f2018-03-05 22:55:17 -0500252 data['entries'][subnet] = send
Rodrigo Manyarie3b59b32018-03-02 19:19:14 -0500253 else:
254 print("%-21s %6d" % (subnet, send))
255
256 if args.json:
257 print(json.dumps(data))
258
259 ipv4_send_bytes.clear()
260
261 if exiting:
262 exit(0)