You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
7.6 KiB
262 lines
7.6 KiB
#!/usr/bin/python |
|
# @lint-avoid-python-3-compatibility-imports |
|
# |
|
# tcpsubnet Summarize TCP bytes sent to different subnets. |
|
# For Linux, uses BCC, eBPF. Embedded C. |
|
# |
|
# USAGE: tcpsubnet [-h] [-v] [-J] [-f FORMAT] [-i INTERVAL] [subnets] |
|
# |
|
# This uses dynamic tracing of kernel functions, and will need to be updated |
|
# to match kernel changes. |
|
# |
|
# This is an adaptation of tcptop from written by Brendan Gregg. |
|
# |
|
# WARNING: This traces all send at the TCP level, and while it |
|
# summarizes data in-kernel to reduce overhead, there may still be some |
|
# overhead at high TCP send/receive rates (eg, ~13% of one CPU at 100k TCP |
|
# events/sec. This is not the same as packet rate: funccount can be used to |
|
# count the kprobes below to find out the TCP rate). Test in a lab environment |
|
# first. If your send rate is low (eg, <1k/sec) then the overhead is |
|
# expected to be negligible. |
|
# |
|
# Copyright 2017 Rodrigo Manyari |
|
# Licensed under the Apache License, Version 2.0 (the "License") |
|
# |
|
# 03-Oct-2017 Rodrigo Manyari Created this based on tcptop. |
|
# 13-Feb-2018 Rodrigo Manyari Fix pep8 errors, some refactoring. |
|
# 05-Mar-2018 Rodrigo Manyari Add date time to output. |
|
|
|
import argparse |
|
import json |
|
import logging |
|
import struct |
|
import socket |
|
from bcc import BPF |
|
from datetime import datetime as dt |
|
from time import sleep |
|
|
|
# arguments |
|
examples = """examples: |
|
./tcpsubnet # Trace TCP sent to the default subnets: |
|
# 127.0.0.1/32,10.0.0.0/8,172.16.0.0/12, |
|
# 192.168.0.0/16,0.0.0.0/0 |
|
./tcpsubnet -f K # Trace TCP sent to the default subnets |
|
# aggregated in KBytes. |
|
./tcpsubnet 10.80.0.0/24 # Trace TCP sent to 10.80.0.0/24 only |
|
./tcpsubnet -J # Format the output in JSON. |
|
""" |
|
|
|
default_subnets = "127.0.0.1/32,10.0.0.0/8," \ |
|
"172.16.0.0/12,192.168.0.0/16,0.0.0.0/0" |
|
|
|
parser = argparse.ArgumentParser( |
|
description="Summarize TCP send and aggregate by subnet", |
|
formatter_class=argparse.RawDescriptionHelpFormatter, |
|
epilog=examples) |
|
parser.add_argument("subnets", help="comma separated list of subnets", |
|
type=str, nargs="?", default=default_subnets) |
|
parser.add_argument("-v", "--verbose", action="store_true", |
|
help="output debug statements") |
|
parser.add_argument("-J", "--json", action="store_true", |
|
help="format output in JSON") |
|
parser.add_argument("--ebpf", action="store_true", |
|
help=argparse.SUPPRESS) |
|
parser.add_argument("-f", "--format", default="B", |
|
help="[bkmBKM] format to report: bits, Kbits, Mbits, bytes, " + |
|
"KBytes, MBytes (default B)", choices=["b", "k", "m", "B", "K", "M"]) |
|
parser.add_argument("-i", "--interval", default=1, type=int, |
|
help="output interval, in seconds (default 1)") |
|
args = parser.parse_args() |
|
|
|
level = logging.INFO |
|
if args.verbose: |
|
level = logging.DEBUG |
|
|
|
logging.basicConfig(level=level) |
|
|
|
logging.debug("Starting with the following args:") |
|
logging.debug(args) |
|
|
|
# args checking |
|
if int(args.interval) <= 0: |
|
logging.error("Invalid interval, must be > 0. Exiting.") |
|
exit(1) |
|
else: |
|
args.interval = int(args.interval) |
|
|
|
# map of supported formats |
|
formats = { |
|
"b": lambda x: (x * 8), |
|
"k": lambda x: ((x * 8) / 1024), |
|
"m": lambda x: ((x * 8) / pow(1024, 2)), |
|
"B": lambda x: x, |
|
"K": lambda x: x / 1024, |
|
"M": lambda x: x / pow(1024, 2) |
|
} |
|
|
|
# Let's swap the string with the actual numeric value |
|
# once here so we don't have to do it on every interval |
|
formatFn = formats[args.format] |
|
|
|
# define the basic structure of the BPF program |
|
bpf_text = """ |
|
#include <uapi/linux/ptrace.h> |
|
#include <net/sock.h> |
|
#include <bcc/proto.h> |
|
|
|
struct index_key_t { |
|
u32 index; |
|
}; |
|
|
|
BPF_HASH(ipv4_send_bytes, struct index_key_t); |
|
|
|
int kprobe__tcp_sendmsg(struct pt_regs *ctx, struct sock *sk, |
|
struct msghdr *msg, size_t size) |
|
{ |
|
u16 family = sk->__sk_common.skc_family; |
|
|
|
if (family == AF_INET) { |
|
u32 dst = sk->__sk_common.skc_daddr; |
|
unsigned categorized = 0; |
|
__SUBNETS__ |
|
} |
|
return 0; |
|
} |
|
""" |
|
|
|
|
|
# Takes in a mask and returns the integer equivalent |
|
# e.g. |
|
# mask_to_int(8) returns 4278190080 |
|
def mask_to_int(n): |
|
return ((1 << n) - 1) << (32 - n) |
|
|
|
# Takes in a list of subnets and returns a list |
|
# of tuple-3 containing: |
|
# - The subnet info at index 0 |
|
# - The addr portion as an int at index 1 |
|
# - The mask portion as an int at index 2 |
|
# |
|
# e.g. |
|
# parse_subnets([10.10.0.0/24]) returns |
|
# [ |
|
# ['10.10.0.0/24', 168427520, 4294967040], |
|
# ] |
|
def parse_subnets(subnets): |
|
m = [] |
|
for s in subnets: |
|
parts = s.split("/") |
|
if len(parts) != 2: |
|
msg = "Subnet [%s] is invalid, please refer to the examples." % s |
|
raise ValueError(msg) |
|
netaddr_int = 0 |
|
mask_int = 0 |
|
try: |
|
netaddr_int = struct.unpack("!I", socket.inet_aton(parts[0]))[0] |
|
except: |
|
msg = ("Invalid net address in subnet [%s], " + |
|
"please refer to the examples.") % s |
|
raise ValueError(msg) |
|
try: |
|
mask_int = int(parts[1]) |
|
except: |
|
msg = "Invalid mask in subnet [%s]. Mask must be an int" % s |
|
raise ValueError(msg) |
|
if mask_int < 0 or mask_int > 32: |
|
msg = ("Invalid mask in subnet [%s]. Must be an " + |
|
"int between 0 and 32.") % s |
|
raise ValueError(msg) |
|
mask_int = mask_to_int(int(parts[1])) |
|
m.append([s, netaddr_int, mask_int]) |
|
return m |
|
|
|
def generate_bpf_subnets(subnets): |
|
template = """ |
|
if (!categorized && (__NET_ADDR__ & __NET_MASK__) == |
|
(dst & __NET_MASK__)) { |
|
struct index_key_t key = {.index = __POS__}; |
|
ipv4_send_bytes.increment(key, size); |
|
categorized = 1; |
|
} |
|
""" |
|
bpf = '' |
|
for i, s in enumerate(subnets): |
|
branch = template |
|
branch = branch.replace("__NET_ADDR__", str(socket.htonl(s[1]))) |
|
branch = branch.replace("__NET_MASK__", str(socket.htonl(s[2]))) |
|
branch = branch.replace("__POS__", str(i)) |
|
bpf += branch |
|
return bpf |
|
|
|
subnets = [] |
|
if args.subnets: |
|
subnets = args.subnets.split(",") |
|
|
|
subnets = parse_subnets(subnets) |
|
|
|
logging.debug("Packets are going to be categorized in the following subnets:") |
|
logging.debug(subnets) |
|
|
|
bpf_subnets = generate_bpf_subnets(subnets) |
|
|
|
# initialize BPF |
|
bpf_text = bpf_text.replace("__SUBNETS__", bpf_subnets) |
|
|
|
logging.debug("Done preprocessing the BPF program, " + |
|
"this is what will actually get executed:") |
|
logging.debug(bpf_text) |
|
|
|
if args.ebpf: |
|
print(bpf_text) |
|
exit() |
|
|
|
b = BPF(text=bpf_text) |
|
|
|
ipv4_send_bytes = b["ipv4_send_bytes"] |
|
|
|
if not args.json: |
|
print("Tracing... Output every %d secs. Hit Ctrl-C to end" % args.interval) |
|
|
|
# output |
|
exiting = 0 |
|
while (1): |
|
|
|
try: |
|
sleep(args.interval) |
|
except KeyboardInterrupt: |
|
exiting = 1 |
|
|
|
# IPv4: build dict of all seen keys |
|
keys = ipv4_send_bytes |
|
for k, v in ipv4_send_bytes.items(): |
|
if k not in keys: |
|
keys[k] = v |
|
|
|
# to hold json data |
|
data = {} |
|
|
|
# output |
|
now = dt.now() |
|
data['date'] = now.strftime('%x') |
|
data['time'] = now.strftime('%X') |
|
data['entries'] = {} |
|
if not args.json: |
|
print(now.strftime('[%x %X]')) |
|
for k, v in reversed(sorted(keys.items(), key=lambda keys: keys[1].value)): |
|
send_bytes = 0 |
|
if k in ipv4_send_bytes: |
|
send_bytes = int(ipv4_send_bytes[k].value) |
|
subnet = subnets[k.index][0] |
|
send = formatFn(send_bytes) |
|
if args.json: |
|
data['entries'][subnet] = send |
|
else: |
|
print("%-21s %6d" % (subnet, send)) |
|
|
|
if args.json: |
|
print(json.dumps(data)) |
|
|
|
ipv4_send_bytes.clear() |
|
|
|
if exiting: |
|
exit(0)
|
|
|