Skip to content

Commit

Permalink
feat(gazer): created TUI to visualize scraped data
Browse files Browse the repository at this point in the history
  • Loading branch information
isala404 committed Oct 2, 2021
1 parent 088df5b commit e0c166a
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 138 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@
# Project Proposal
Project Proposal/*
!Project Proposal/*.tex
!Project Proposal/*.bib
!Project Proposal/*.bib


## Python
**/__pycache__/**
Binary file added gazer/demo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 100 additions & 0 deletions gazer/gazer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import threading
import time
import termplotlib as tpl
import pandas as pd
from bcc import BPF
from socket import inet_ntop, AF_INET
from struct import pack


class Gazer:
request_df = pd.DataFrame(columns=["PID", "COMM", "LADDR", "LPORT", "RADDR", "RPORT", "TX_KB", "RX_KB", "MS"])
syn_df = pd.DataFrame(columns=["backlog", "slot", "saddr", "lport", "value", "outdated"])
bpf_text = ""

def __init__(self):
with open('bpf.c', 'r') as f:
bpf_text = f.read()

with open('sock_state.c', 'r') as f:
bpf_text += f.read()

with open('syn_backlog.c', 'r') as f:
bpf_text += f.read()

bpf_text = bpf_text.replace('FILTER_PID', '')
self.bpf_text = bpf_text.replace('ADDRFILTER', '')
self.b = BPF(text=self.bpf_text)
self.b["ipv4_events"].open_perf_buffer(self.ipv4_request_event, page_cnt=64)
self.syn_backlog_buffer = self.b['syn_backlog']

def ipv4_request_event(self, cpu, data, size):
event = self.b["ipv4_events"].event(data)
self.request_df = self.request_df.append({
"PID": event.pid,
"COMM": event.task.decode('utf-8', 'replace'),
"LADDR": inet_ntop(AF_INET, pack("I", event.saddr)),
"LPORT": event.ports >> 32,
"RADDR": inet_ntop(AF_INET, pack("I", event.daddr)),
"RPORT": event.ports & 0xffffffff,
"TX_KB": event.tx_b,
"RX_KB": event.rx_b,
"MS": float(event.span_us) / 1000,
}, ignore_index=True)
self.request_df = self.request_df[-10:]

def poll_requests(self):
while True:
self.b.perf_buffer_poll()

def poll_syn_backlog(self):
while True:
data = self.syn_backlog_buffer.items()
self.syn_df["outdated"] = True
for row in data:
self.syn_df = self.syn_df.append({
"backlog": row[0].backlog,
"slot": row[0].slot,
"saddr": inet_ntop(AF_INET, pack("I", row[0].saddr)),
"lport": row[0].lport,
"value": row[1].value,
"outdated": False,
}, ignore_index=True)
self.syn_backlog_buffer.clear()
time.sleep(5)

def syn_backlog_text(self):
self.syn_df['saddr_port'] = self.syn_df["saddr"] + ":" + self.syn_df["lport"].astype(str)
grouped_df = self.syn_df.groupby('saddr_port')
out = "SYN Backlog\n"
for key, item in grouped_df:
x, y = [], []
out += f"\n{key}\n"
df = grouped_df.get_group(key)
new_data = df.loc[df['outdated'] == False]

if new_data.empty:
x.append(0)
y.append("0-1")
else:
for entry in new_data.sort_values(by=['slot']).to_dict('records'):
x.append(entry['value'])
y.append(f"{entry['slot'] - 1} -> {entry['slot']}")
fig = tpl.figure()
fig.barh(x, y, force_ascii=True)
out += fig.get_string() + "\n"
return out

def request_log_text(self):
if self.request_df.empty:
return ""
return self.request_df.tail(10).__str__()

def poll_data_in_bg(self):
poll_syn_backlog = threading.Thread(target=self.poll_syn_backlog, args=())
poll_syn_backlog.daemon = True
poll_syn_backlog.start()

poll_requests = threading.Thread(target=self.poll_requests, args=())
poll_requests.daemon = True
poll_requests.start()
203 changes: 66 additions & 137 deletions gazer/main.py
Original file line number Diff line number Diff line change
@@ -1,141 +1,70 @@
#!/usr/bin/python
# @lint-avoid-python-3-compatibility-imports
#
# tcplife Trace the lifespan of TCP sessions and summarize.
# For Linux, uses BCC, BPF. Embedded C.
#
# USAGE: tcplife [-h] [-C] [-S] [-p PID] [-4 | -6] [interval [count]]
#
# This uses the sock:inet_sock_set_state tracepoint if it exists (added to
# Linux 4.16, and replacing the earlier tcp:tcp_set_state), else it uses
# kernel dynamic tracing of tcp_set_state().
#
# While throughput counters are emitted, they are fetched in a low-overhead
# manner: reading members of the tcp_info struct on TCP close. ie, we do not
# trace send/receive.
#
# Copyright 2016 Netflix, Inc.
# Licensed under the Apache License, Version 2.0 (the "License")
#
# IDEA: Julia Evans
#
# 18-Oct-2016 Brendan Gregg Created this.
# 29-Dec-2017 " " Added tracepoint support.

from __future__ import print_function

import curses
import time
from gazer import Gazer


def draw_menu(stdscr: curses.window):
k = 0
cursor_x = 0
cursor_y = 0

# Clear and refresh the screen for a blank canvas
stdscr.clear()
stdscr.refresh()
stdscr.nodelay(True)
# curses.delay_output(100)

# Start colors in curses
curses.start_color()
curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)
curses.init_pair(2, curses.COLOR_RED, curses.COLOR_BLACK)
curses.init_pair(3, curses.COLOR_BLACK, curses.COLOR_WHITE)

# Loop where k is the last character pressed
while k != ord('q'):

# Initialization
stdscr.clear()
height, width = stdscr.getmaxyx()

if k == curses.KEY_DOWN:
cursor_y = cursor_y + 1
elif k == curses.KEY_UP:
cursor_y = cursor_y - 1
elif k == curses.KEY_RIGHT:
cursor_x = cursor_x + 1
elif k == curses.KEY_LEFT:
cursor_x = cursor_x - 1

cursor_x = max(0, cursor_x)
cursor_x = min(width - 1, cursor_x)

cursor_y = max(0, cursor_y)
cursor_y = min(height - 1, cursor_y)

statusbarstr = "Press 'q' to exit | STATUS BAR | Pos: {}, {}".format(cursor_x, cursor_y)

stdscr.addstr(0, 0, "Requests", curses.color_pair(1))
stdscr.addstr(2, 0, gazer.request_log_text(), curses.color_pair(1))
stdscr.addstr(15, 0, gazer.syn_backlog_text(), curses.color_pair(1))

# Render status bar
stdscr.attron(curses.color_pair(3))
stdscr.addstr(height - 1, 0, statusbarstr)
stdscr.addstr(height - 1, len(statusbarstr), " " * (width - len(statusbarstr) - 1))
stdscr.attroff(curses.color_pair(3))

stdscr.move(cursor_y, cursor_x)

# Refresh the screen
stdscr.refresh()

# Wait for next input
k = stdscr.getch()
time.sleep(1 / 30)

from bcc import BPF
import argparse
from socket import inet_ntop, AF_INET, inet_aton
from struct import pack, unpack

# arguments
examples = """examples:
./tcplife # trace all TCP connect()s
./tcplife -T # include time column (HH:MM:SS)
./tcplife -w # wider columns (fit IPv6)
./tcplife -stT # csv output, with times & timestamps
./tcplife -p 181 # only trace PID 181
./tcplife -L 80 # only trace local port 80
./tcplife -L 80,81 # only trace local ports 80 and 81
./tcplife -D 80 # only trace remote port 80
./tcplife -4 # only trace IPv4 family
./tcplife -6 # only trace IPv6 family
"""
parser = argparse.ArgumentParser(
description="Trace the lifespan of TCP sessions and summarize",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=examples)
parser.add_argument("-w", "--wide", action="store_true",
help="wide column output (fits IPv6 addresses)")
parser.add_argument("-p", "--pid",
help="trace this PID only")
parser.add_argument("-a", "--addr",
help="filter for address")
parser.add_argument("--ebpf", action="store_true",
help=argparse.SUPPRESS)
args = parser.parse_args()
debug = 0

# define BPF program
with open('bpf.c', 'r') as f:
bpf_text = f.read()

with open('sock_state.c', 'r') as f:
bpf_text += f.read()

with open('syn_backlog.c', 'r') as f:
bpf_text += f.read()

# code substitutions
if args.pid:
bpf_text = bpf_text.replace('FILTER_PID',
'if (pid != %s) { return 0; }' % args.pid)

if args.addr:
bpf_text = bpf_text.replace('ADDRFILTER',
"""if (data4.saddr != {0} || data4.daddr != {0})
return 0;""".format(unpack("=I", inet_aton(args.addr))[0]))

bpf_text = bpf_text.replace('FILTER_PID', '')
bpf_text = bpf_text.replace('ADDRFILTER', '')

#
# Setup output formats
#
# Don't change the default output (next 2 lines): this fits in 80 chars. I
# know it doesn't have NS or UIDs etc. I know. If you really, really, really
# need to add columns, columns that solve real actual problems, I'd start by
# adding an extended mode (-x) to included those columns.
#
header_string = "%-5s %-10.10s %s%-15s %-5s %-15s %-5s %5s %5s %s"
format_string = "%-5d %-10.10s %s%-15s %-5d %-15s %-5d %5d %5d %.2f"


# process event
def print_ipv4_event(cpu, data, size):
event = b["ipv4_events"].event(data)
print(format_string % (event.pid, event.task.decode('utf-8', 'replace'),
"",
inet_ntop(AF_INET, pack("I", event.saddr)), event.ports >> 32,
inet_ntop(AF_INET, pack("I", event.daddr)), event.ports & 0xffffffff,
event.tx_b, event.rx_b, float(event.span_us) / 1000))


# initialize BPF
b = BPF(text=bpf_text)

# header
print(header_string % ("PID", "COMM", "", "LADDR",
"LPORT", "RADDR", "RPORT", "TX_KB", "RX_KB", "MS"))

start_ts = 0

# read events
b["ipv4_events"].open_perf_buffer(print_ipv4_event, page_cnt=64)
# b.attach_kprobe(event="tcp_v4_syn_recv_sock", fn_name="update_syn_backlog")
while 1:
try:
b.perf_buffer_poll()
except KeyboardInterrupt:
# exit()
break

# while True:
# try:
# time.sleep(999999)
# except KeyboardInterrupt:
# break

dist = b['syn_backlog']
print()
for item in dist.items():
print({"backlog": item[0].backlog,
"slot": item[0].slot,
"saddr": inet_ntop(AF_INET, pack("I", item[0].saddr)),
"lport": item[0].lport,
"value": item[1].value})

gazer = Gazer()
gazer.poll_data_in_bg()

curses.wrapper(draw_menu)

0 comments on commit e0c166a

Please sign in to comment.