#!/usr/bin/env python # # Email: Hagen Paul Pfeifer <hagen@jauu.net> # URL: http://research.protocollabs.com/captcp/ # Captcp is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Captcp is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Captcp. If not, see <http://www.gnu.org/licenses/>. from __future__ import print_function import sys import os import logging import optparse import dpkt import socket import struct import inspect import math import pprint import time import datetime import subprocess import select import re import shutil import distutils.dir_util import string # optional packages try: import GeoIP except ImportError: GeoIP = None try: import cairo except ImportError: cairo = None try: import numpy except ImportError: numpy = None try: import wave except ImportError: wave = None pp = pprint.PrettyPrinter(indent=4) # Required debian packages: # python-dpkt # Suggested debian packages: # python-cairo # Optional debian packages: # python-geoip # python-numpy __programm__ = "captcp" __author__ = "Hagen Paul Pfeifer" __version__ = "1.7" __license__ = "GPLv3" # custom exceptions class ArgumentException(Exception): pass class InternalSequenceException(Exception): pass class InternalException(Exception): pass class SequenceContainerException(InternalException): pass class NotImplementedException(InternalException): pass class SkipProcessStepException(Exception): pass class PacketNotSupportedException(Exception): pass class UnitException(Exception): pass # IP flag constants IP_TOS_ECT = dpkt.ip.IP_TOS_ECT IP_TOS_CE = dpkt.ip.IP_TOS_CE # TCP flag constants TH_URG = dpkt.tcp.TH_URG TH_ACK = dpkt.tcp.TH_ACK TH_PSH = dpkt.tcp.TH_PUSH TH_RST = dpkt.tcp.TH_RST TH_SYN = dpkt.tcp.TH_SYN TH_FIN = dpkt.tcp.TH_FIN TH_ECE = dpkt.tcp.TH_ECE TH_CWR = dpkt.tcp.TH_CWR # "Robust Explicit Congestion Notification (ECN) # Signaling with Nonces" (RFC 3540) specifies an # additional ECN Flag: NS which is out of the 8 bit # flags section, shared with header length field. I # emailed Jon Oberheide to get some valuable solutions. # # See http://tools.ietf.org/html/rfc3540#section-9 # Protocols TCP = dpkt.tcp.TCP UDP = dpkt.udp.UDP # Units (bit): # kilobit (kbit) 10^3 - kibibit (Kibit) 2^10 # megabit (Mbit) 10^6 - mebibit (Mibit) 2^20 # gigabit (Gbit) 10^9 - gibibit (Gibit) 2^30 # # Units (byte): # kilobyte (kB) 10^3 - kibibyte (KiB) 2^10 # megabyte (MB) 10^6 - mebibyte (MiB) 2^20 # gigabyte (GB) 10^9 - gibibyte (GiB) 2^30 class ExitCodes: EXIT_SUCCESS = 0 EXIT_ERROR = 1 EXIT_CMD_LINE = 2 EXIT_ENVIRONMENT = 3 EXIT_PLATFORM = 4 class Info: ETHERNET_HEADER_LEN = 14 class SequenceContainer: class Container: pass def __init__(self): self.sequnce_list = list() if not numpy: self.logger.error("Python numpy module not installed - but required") sys.exit(ExitCodes.EXIT_CMD_LINE) def __len__(self): return len(self.sequnce_list) def print_list(self): for i in self.sequnce_list: sys.stdout.write(str(i.left_edge) + "-" + str(i.right_edge) + "\n" ) @staticmethod def uint32_add(num, summand): """ the caller (Module) must make sure that module is available """ s1 = numpy.array(num, dtype=numpy.dtype('uint32')) s2 = numpy.array(summand, dtype=numpy.dtype('uint32')) return int(numpy.array((s1 + s2), dtype=numpy.dtype('uint32'))) @staticmethod def uint32_sub(num, summand): """ the caller (Module) must make sure that module is available """ s1 = numpy.array(num, dtype=numpy.dtype('uint32')) s2 = numpy.array(summand, dtype=numpy.dtype('uint32')) return int(numpy.array((s1 - s2), dtype=numpy.dtype('uint32'))) def before(self, seq1, seq2): s1 = numpy.array(seq1, dtype=numpy.dtype('uint32')) s2 = numpy.array(seq2, dtype=numpy.dtype('uint32')) res = numpy.array((s1 - s2), dtype=numpy.dtype('int32')) if res < 0: return True else: return False def after(self, seq1, seq2): return self.before(seq2, seq1) # is s2 <= s1 <= s3 def between(self, seq1, seq2, seq3): s1 = numpy.array(seq1, dtype=numpy.dtype('uint32')) s2 = numpy.array(seq2, dtype=numpy.dtype('uint32')) s3 = numpy.array(seq3, dtype=numpy.dtype('uint32')) if s3 - s2 >= s1 - s2: return True else: return False def add_sequence(self, array): if len(array) != 2: raise ArgumentException("array must contain excatly 2 members") new = SequenceContainer.Container() new.left_edge = array[0] new.right_edge = array[1] if len(self.sequnce_list) <= 0: self.sequnce_list.append(new) return # if new element is the direct neighbour we merge if new.left_edge == self.sequnce_list[-1].right_edge + 1: self.sequnce_list[-1].right_edge = new.right_edge del new return # if new element is far right we add it instantly if self.sequnce_list[-1].right_edge < new.left_edge + 1: self.sequnce_list.append(new) return # check if not new.left_edge <= self.sequnce_list[-1].right_edge: raise SequenceContainerException("internal error") reverse_enumerate = lambda l: \ itertools.izip(xrange(len(l)-1, -1, -1), reversed(l)) it = reverse_enumerate(self.sequnce_list) # ok, the new packet is within the packets while True: try: (i, old) = it.next() # match the segment exactply? if (old.left_edge - 1 == new.right_edge and self.sequnce_list[i - 1].right_edge + 1 == new.left_edge): self.sequnce_list.remove(old) self.sequnce_list[i - 1].right_edge = old.right_edge return # check if the new packet match between a gap if (old.left_edge > new.right_edge and self.sequnce_list[i - 1].right_edge < new.left_edge): self.sequnce_list.insert(i, new) return # can we merge one one side at least? (one end close, one new gap) if old.left_edge - 1 == new.right_edge: if len(self.sequnce_list) <= 1: old.left_edge = new.left_edge del new return elif self.sequnce_list[i - 1].right_edge < new.left_edge: old.left_edge = new.left_edge del new return except: break class U: """ Utility module, to collect usefull functionality needed by several other classes. We name it U to make it short and non bloated""" @staticmethod def byte_to_unit(byte, unit): byte = float(byte) if unit == "byte" or unit == "Byte": return byte if unit == "bit" or unit == "Bit": return byte * 8 if unit == "kB" or unit == "kilobyte": return byte / 1000 if unit == "MB" or unit == "megabyte": return byte / (1000 * 1000) if unit == "GB" or unit == "gigabyte": return byte / (1000 * 1000 * 1000) if unit == "KiB" or unit == "kibibyte": return byte / 1024 if unit == "MiB" or unit == "mebibyte": return byte / (1024 * 1024) if unit == "GiB" or unit == "gibibyte": return byte / (1024 * 1024 * 1024) if unit == "kbit" or unit == "kilobit": return byte * 8 / 1000 if unit == "Mbit" or unit == "megabit": return byte * 8 / (1000 * 1000) if unit == "Gbit" or unit == "gigabit": return byte * 8 / (1000 * 1000 * 1000) if unit == "KiBit" or unit == "kibibit": return byte * 8 / 1024 if unit == "MiBit" or unit == "mebibit": return byte * 8 / (1024 * 1024) if unit == "GiBit" or unit == "gibibit": return byte * 8 / (1024 * 1024 * 1024) raise UnitException("unit %s not known" % (unit)) @staticmethod def best_match(byte): last_unit = "bit" last_val = float(byte) * 8 units = ("kbit", "Mbit", "Gbit") for unit in units: val = U.byte_to_unit(byte, unit) if val < 1.0: return "%.2f %s" % (last_val, last_unit) last_unit = unit last_val = val return "%.2f %s" % (U.byte_to_unit(byte, "Gbit"), "Gbit") @staticmethod def percent(a, b): if b == 0: return 0.0 return float(a) / b * 100 @staticmethod def copytree(src, dst, symlinks=False, ignore=None): names = os.listdir(src) if ignore is not None: ignored_names = ignore(src, names) else: ignored_names = set() os.makedirs(dst) errors = [] for name in names: if name in ignored_names: continue srcname = os.path.join(src, name) dstname = os.path.join(dst, name) try: if symlinks and os.path.islink(srcname): linkto = os.readlink(srcname) os.symlink(linkto, dstname) elif os.path.isdir(srcname): copytree(srcname, dstname, symlinks, ignore) else: copy2(srcname, dstname) except (IOError, os.error) as why: errors.append((srcname, dstname, str(why))) # catch the Error from the recursive copytree so that we can # continue with other files except Error as err: errors.extend(err.args[0]) try: copystat(src, dst) except WindowsError: # can't copy file access times on Windows pass except OSError as why: errors.extend((src, dst, str(why))) if errors: raise Error(errors) class RainbowColor: ANSI = 0 ANSI256 = 1 HEX = 2 DISABLE = 3 def __init__(self, mode=ANSI256): if mode == RainbowColor.ANSI: self.init_color_ansi() elif mode == RainbowColor.ANSI256: self.init_color_ansi256() elif mode == RainbowColor.HEX: self.init_color_hex() elif mode == RainbowColor.DISABLE: self.init_color_none() else: raise Exception() # this color should not be printed self.skip_list = ['red', 'end'] def __getitem__(self, key): return self.color_palette[key] def init_color_none(self): self.color_palette = {'red':'', 'green':'', 'end':'' } def init_color_hex(self): self.color_palette = {'red':'#ff0000', 'green':'#00ff00', 'end':''} def init_color_ansi256(self): self.color_palette = dict() for i in range(255): self.color_palette[i + 1] = '\033[38;5;%dm' % (i + 1) self.color_palette['end'] = '\033[0m' self.color_palette['red'] = '\033[91m' del self.color_palette[1] del self.color_palette[9] del self.color_palette[52] del self.color_palette[88] del self.color_palette[89] del self.color_palette[124] del self.color_palette[125] del self.color_palette[126] del self.color_palette[127] del self.color_palette[160] del self.color_palette[161] del self.color_palette[162] del self.color_palette[163] del self.color_palette[196] del self.color_palette[197] del self.color_palette[198] del self.color_palette[199] del self.color_palette[200] def init_color_ansi(self): self.color_palette = { 'yellow':'\033[0;33;40m', 'foo':'\033[93m', 'red':'\033[91m', 'green':'\033[92m', 'blue':'\033[94m', 'end':'\033[0m' } def next(self): if self.color_palette_pos >= len(self.color_palette_flat): raise StopIteration retdata = self.color_palette_flat[self.color_palette_pos] self.color_palette_pos += 1 return retdata def infinite_next(self): while True: found = True retdata = self.color_palette_flat[self.color_palette_pos % \ (len(self.color_palette_flat))] self.color_palette_pos += 1 for i in self.skip_list: if i in self.color_palette and self.color_palette[i] == retdata: found = False break if found: return retdata def __iter__(self): self.color_palette_pos = 0 self.color_palette_flat = self.color_palette.values() return self class Utils: @staticmethod def ts_tofloat(ts): return float(ts.seconds) + ts.microseconds / 1E6 + ts.days * 86400 class Converter: def dotted_quad_num(ip): "convert decimal dotted quad string to long integer" return struct.unpack('I', socket.inet_aton(ip))[0] dotted_quad_num = staticmethod(dotted_quad_num) def num_to_dotted_quad(n): "convert long int to dotted quad string" return socket.inet_ntoa(struct.pack('I', n)) num_to_dotted_quad = staticmethod(num_to_dotted_quad) def make_mask(n): "return a mask of n bits as a long integer" return (1L << n)-1 make_mask = staticmethod(make_mask) def dpkt_addr_to_string(addr): if len(addr) == 16: # IPv6 # FIXME: inet_ntop is UNIX only according to the python doc return socket.inet_ntop(socket.AF_INET6, addr) else: # IPv4 iaddr = int(struct.unpack('I', addr)[0]) return Converter.num_to_dotted_quad(iaddr) dpkt_addr_to_string = staticmethod(dpkt_addr_to_string) def ip_to_net_host(ip, maskbits): "returns tuple (network, host) dotted-quad addresses given IP and mask size" n = Converter.dotted_quad_num(ip) m = Converter.makeMask(maskbits) host = n & m net = n - host return Converter.num_to_dotted_quad(net), Converter.num_to_dotted_quad(host) ip_to_net_host = staticmethod(ip_to_net_host) class PcapParser: def __init__(self, pcap_file_path, pcap_filter): self.logger = logging.getLogger() self.pcap_file = False try: self.pcap_file = open(pcap_file_path) except IOError: self.logger.error("Cannot open pcap file: %s" % (pcap_file_path)) sys.exit(ExitCodes.EXIT_ERROR) self.pc = dpkt.pcap.Reader(self.pcap_file) try: self.decode = { dpkt.pcap.DLT_LOOP: dpkt.loopback.Loopback, dpkt.pcap.DLT_NULL: dpkt.loopback.Loopback, dpkt.pcap.DLT_EN10MB: dpkt.ethernet.Ethernet, dpkt.pcap.DLT_IEEE802: dpkt.ethernet.Ethernet, dpkt.pcap.DLT_PPP: dpkt.ppp.PPP, dpkt.pcap.DLT_LINUX_SLL: dpkt.sll.SLL }[self.pc.datalink()] except KeyError: self.logger.error("Packet link type not know (%d)! " "Interpret at Ethernet now - but be carefull!" % ( self.pc.datalink())) self.decode = dpkt.ethernet.Ethernet if pcap_filter: self.pc.setfilter(pcap_filter) def __del__(self): if self.pcap_file: self.pcap_file.close() def register_callback(self, callback): self.callback = callback def packet_len_error(self, snaplen, packet_len): self.logger.critical("Captured data was too short (packet: %d, snaplen: %d)" " - please recapture with snaplen of 0: infinity" % (packet_len, snaplen)) def run(self): try: for ts, pkt in self.pc: if self.pc.snaplen < len(pkt): self.packet_len_error(self.pc.snaplen, len(pkt)) sys.exit(1) packet = self.decode(pkt) dt = datetime.datetime.fromtimestamp(ts) self.callback(dt, packet.data) except SkipProcessStepException: self.logger.debug("skip processing step") class PacketInfo: @staticmethod def is_tcp(packet): if type(packet) != dpkt.ip.IP and type(packet) != dpkt.ip6.IP6: return False if type(packet.data) == TCP: return True return False @staticmethod def is_udp(packet): if type(packet) != dpkt.ip.IP and type(packet) != dpkt.ip6.IP6: return False if type(packet.data) == UDP: return True return False class IpPacketInfo(PacketInfo): def __init__(self, packet, module=None): self.tcp = packet.data if type(self.tcp) != TCP: raise InternalException("Only TCP packets are allowed") if module != None and not isinstance(module, Mod): raise InternalException( "Argument module must be a subclass of module (not %s)" % (type(module))) if type(packet) == dpkt.ip.IP: self.sip = Converter.dpkt_addr_to_string(packet.src) self.dip = Converter.dpkt_addr_to_string(packet.dst) self.ipversion = "IP " elif type(packet) == dpkt.ip6.IP6: self.sip = socket.inet_ntop(socket.AF_INET6, packet.src) self.dip = socket.inet_ntop(socket.AF_INET6, packet.dst) self.ipversion = "IP6" else: raise InternalException("unknown protocol") self.sport = int(self.tcp.sport) self.dport = int(self.tcp.dport) self.seq = int(self.tcp.seq) self.ack = int(self.tcp.ack) self.win = int(self.tcp.win) self.urp = int(self.tcp.urp) self.sum = int(self.tcp.sum) class TcpPacketInfo(PacketInfo): class TcpOptions: def __init__(self): self.data = dict() def __getitem__(self, key): return self.data[key] def __setitem__(self, key, val): self.data[key] = val def __init__(self, packet, module=None): self.tcp = packet.data if type(self.tcp) != TCP: raise InternalException("Only TCP packets are allowed") if module != None and not isinstance(module, Mod): raise InternalException( "Argument module must be a subclass of module (not %s)" % (type(module))) if type(packet) == dpkt.ip.IP: self.sip = Converter.dpkt_addr_to_string(packet.src) self.dip = Converter.dpkt_addr_to_string(packet.dst) self.ipversion = "IP " elif type(packet) == dpkt.ip6.IP6: self.sip = socket.inet_ntop(socket.AF_INET6, packet.src) self.dip = socket.inet_ntop(socket.AF_INET6, packet.dst) self.ipversion = "IP6" else: raise InternalException("unknown protocol") self.sport = int(self.tcp.sport) self.dport = int(self.tcp.dport) self.seq = int(self.tcp.seq) self.ack = int(self.tcp.ack) self.win = int(self.tcp.win) self.urp = int(self.tcp.urp) self.sum = int(self.tcp.sum) self.parse_tcp_options() def caller(self): stack = inspect.stack() sys.stderr.write("%s" % (stack)) frame = stack[2][0] caller = frame.f_locals.get('self', None) return caller def is_ack_flag(self): return self.tcp.flags & TH_ACK def is_syn_flag(self): return self.tcp.flags & TH_SYN def is_urg_flag(self): return self.tcp.flags & TH_URG def is_psh_flag(self): return self.tcp.flags & TH_PSH def is_fin_flag(self): return self.tcp.flags & TH_FIN def is_rst_flag(self): return self.tcp.flags & TH_RST def is_ece_flag(self): return self.tcp.flags & TH_ECE def is_cwr_flag(self): return self.tcp.flags & TH_CWR def create_flag_brakets(self): s = "[" if self.is_cwr_flag(): s += "C" if self.is_ece_flag(): s += "E" if self.is_urg_flag(): s += "U" if self.is_ack_flag(): s += "A" if self.is_psh_flag(): s += "P" if self.is_rst_flag(): s += "R" if self.is_syn_flag(): s += "S" if self.is_fin_flag(): s += "F" s += "]" return s def construct_tcp_options_label(self): ret = "" if self.options['mss']: ret += "mss: %d" % (self.options['mss']) if self.options['wsc']: ret += "wsc: %d" % (self.options['wsc']) if self.options['tsval'] and self.options['tsecr']: ret += "ts: %d:%d" % (self.options['tsval'], self.options['tsecr']) if self.options['sackok']: ret += "sackok" def linear_sackblocks_array(self, liste): retlist = list() i = len(liste) / 2 while i > 0: r = list() r.append(liste.pop(-1)) r.append(liste.pop(-1)) retlist.append(r) i -= 1 return retlist def parse_tcp_options(self): self.options = TcpPacketInfo.TcpOptions() self.options['mss'] = False self.options['wsc'] = False self.options['tsval'] = False self.options['tsecr'] = False self.options['sackok'] = False self.options['sackblocks'] = False opts = [] for opt in dpkt.tcp.parse_opts(self.tcp.opts): try: o, d = opt if len(d) > 32: raise TypeError except TypeError: break if o == dpkt.tcp.TCP_OPT_MSS: self.options['mss'] = struct.unpack('>H', d)[0] elif o == dpkt.tcp.TCP_OPT_WSCALE: self.options['wsc'] = ord(d) elif o == dpkt.tcp.TCP_OPT_SACKOK: self.options['sackok'] = True elif o == dpkt.tcp.TCP_OPT_SACK: ofmt="!%sI" % int(len(d) / 4) self.options['sackblocks'] = self.linear_sackblocks_array(list(struct.unpack(ofmt, d))) elif o == dpkt.tcp.TCP_OPT_TIMESTAMP: (self.options['tsval'], self.options['tsecr']) = struct.unpack('>II', d) opts.append(o) class CaptureLevel: LINK_LAYER = 0 NETWORK_LAYER = 1 TRANSPORT_LAYER = 2 class Mod: def register_captcp(self, captcp): self.captcp = captcp def __init__(self): self.captcp = None self.cc = ConnectionContainer() self.capture_level = CaptureLevel.NETWORK_LAYER self.logger = logging.getLogger() def internal_pre_process_packet(self, ts, packet): """ this is a hidden preprocessing function, called for every packet""" assert(self.captcp != None) self.cc.update(ts, packet) self.pre_process_packet(ts, packet) def initialize(self): """ called at the very beginning of module lifetime""" pass def pre_process_packet(self, ts, packet): """ final packet round""" # We simple cannot skip this processing step because it # is internally required for accounting. The worst case # scenario imagineable where pre_process_packet() is skipped # and later in process_packet the connection is required - which # in turn was never initialized. So we always pre_process_packet # here. # There ARE solutions to optimize this case - sure. But I skipped # this because I never run into performance problems. pass def pre_process_final(self): """ single call between pre_process_packet and process_packet to do some calc""" pass def process_packet(self, ts, packet): """ final packet round""" raise SkipProcessStepException() def process_final(self): """ called at the end of packet processing""" pass def set_opts_logevel(self): if not self.opts: raise InternalException("Cannot call set_opts_logevel() if no " + "options parsing was done") if not self.opts.loglevel: """ this is legitim: no loglevel specified""" return if self.opts.loglevel == "debug": self.logger.setLevel(logging.DEBUG) elif self.opts.loglevel == "info": self.logger.setLevel(logging.INFO) elif self.opts.loglevel == "warning": self.logger.setLevel(logging.WARNING) elif self.opts.loglevel == "error": self.logger.setLevel(logging.ERROR) else: raise ArgumentException("loglevel \"%s\" not supported" % self.opts.loglevel) class Geoip(Mod): def initialize(self): self.parse_local_options() def parse_local_options(self): parser = optparse.OptionParser() parser.usage = "geoip" parser.add_option( "-v", "--loglevel", dest="loglevel", default=None, type="string", help="show verbose") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: self.logger.error("no IP address argument given, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.ip_address = args[2] self.logger.info("# ip address: \"%s\"" % self.ip_address) def process_final(self): if not GeoIP: self.logger.info("GeoIP package not installed on system, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) gi = GeoIP.new(GeoIP.GEOIP_MEMORY_CACHE) sys.stdout.write("Country Code: " + gi.country_code_by_addr(self.ip_address) + "\n") class PayloadTimePortMod(Mod): PORT_START = 0 PORT_END = 65535 DEFAULT_VAL = 0.0 def initialize(self): self.parse_local_options() self.data = dict() self.trace_start = None def parse_local_options(self): parser = optparse.OptionParser() parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-f", "--format", dest="format", default="3ddata", type="string", help="the data format for gnuplot") parser.add_option( "-p", "--port", dest="port", default="sport", type="string", help="sport or dport") parser.add_option( "-s", "--sampling", dest="sampling", default=50, type="int", help="sampling rate (default: 5 seconds)") parser.add_option( "-o", "--outfile", dest="outfile", default="payload-time-port.data", type="string", help="name of the output file (default: payload-time-port.data)") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: sys.stderr.write("no pcap file argument given, exiting\n") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.captcp.pcap_file_path = args[2] self.logger.info("pcap file: %s" % (self.captcp.pcap_file_path)) def process_packet(self, ts, packet): try: ip = packet tcp = packet.data except AttributeError: self.logger.warning("Packet at %s could not be parsed, skipping" % ts) return if type(tcp) != TCP: return time = Utils.ts_tofloat(ts - self.cc.capture_time_start) if self.trace_start == None: self.trace_start = time self.time_offset = time self.next_sampling_boundary = time + float(self.opts.sampling) if time > self.next_sampling_boundary: self.next_sampling_boundary = time + float(self.opts.sampling) if self.next_sampling_boundary - self.time_offset not in self.data: self.data[self.next_sampling_boundary - self.time_offset] = dict() dport = int(tcp.dport) sport = int(tcp.sport) if dport not in self.data[self.next_sampling_boundary - self.time_offset]: self.data[self.next_sampling_boundary - self.time_offset][dport] = dict() self.data[self.next_sampling_boundary - self.time_offset][dport]["cnt"] = 0 self.data[self.next_sampling_boundary - self.time_offset][dport]["sum"] = 0 self.data[self.next_sampling_boundary - self.time_offset][dport]["sum"] += len(packet) self.data[self.next_sampling_boundary - self.time_offset][dport]["cnt"] += 1 if sport not in self.data[self.next_sampling_boundary - self.time_offset]: self.data[self.next_sampling_boundary - self.time_offset][sport] = dict() self.data[self.next_sampling_boundary - self.time_offset][sport]["cnt"] = 0 self.data[self.next_sampling_boundary - self.time_offset][sport]["sum"] = 0 self.data[self.next_sampling_boundary - self.time_offset][sport]["sum"] += len(packet) self.data[self.next_sampling_boundary - self.time_offset][sport]["cnt"] += 1 def print_data(self): for timesortedtupel in sorted(self.data.iteritems(), key = lambda (k,v): float(k)): time = timesortedtupel[0] for port in range(PayloadTimePortMod.PORT_END + 1): if port in timesortedtupel[1]: avg = float(timesortedtupel[1][port]["sum"]) / float(timesortedtupel[1][port]["cnt"]) sys.stdout.write(str(time) + " " + str(port) + " " + str(avg) + "\n") else: pass sys.stdout.write(str(time) + " " + str(port) + " " + str(PayloadTimePortMod.DEFAULT_VAL) + "\n") sys.stdout.write("\n") def process_final(self): self.print_data() class TemplateMod(Mod): class TemplateContainer: pass TYPE_MAKEFILE = 1 TYPE_GNUPLOT = 2 def __init__(self): Mod.__init__(self) self.init_db() def initialize(self): self.parse_local_options() def get_content_by_name(self, name): pathname = False for i in self.db: if i.name == name: pathname = i.full_path break if not pathname: self.logger.error("template %s not valid" % name) return fd = open(pathname, 'r') data = fd.read() fd.close() return data def init_db(self): self.logger.debug("initialize local template database") path = "%s/data/templates/" % (os.path.dirname(os.path.realpath(__file__))) self.db = [] listing = os.listdir(path) for files in listing: m = re.match(r"(.*)\.(\w+)", files) if not m.group(0) and not m.group(1): self.logger.error("strange files show up in %s!" % (path)) continue tc = TemplateMod.TemplateContainer() tc.full_path = "%s%s" % (path, files) if m.group(2) == "gpi": tc.type = TemplateMod.TYPE_GNUPLOT tc.name = m.group(1) elif m.group(2) == "make": tc.type = TemplateMod.TYPE_MAKEFILE tc.name = m.group(1) else: # catch unknown file formats (e.g. *.m - matlab files) tc.type = str() tc.name = str() self.db.append(tc) def print_available_templates(self): gpi = list(); mak = list() for i in self.db: if i.type == TemplateMod.TYPE_MAKEFILE: mak.append(i) elif i.type == TemplateMod.TYPE_GNUPLOT: gpi.append(i) else: continue sys.stdout.write("\nmakefile templates:\n") for i in mak: sys.stdout.write("\t%s\n" % (i.name)) sys.stdout.write("\ngnuplot templates:\n") for i in gpi: sys.stdout.write("\t%s\n" % (i.name)) def parse_local_options(self): self.width = self.height = 0 parser = optparse.OptionParser() parser.usage = "%prog template [options] <templatename>" parser.add_option( "-v", "--loglevel", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-o", "--output-dir", dest="outputdir", default=None, type="string", help="specify the output directory") parser.add_option( "-l", "--list", dest="list", default=False, action="store_true", help="list all available templates") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: if self.opts.list: self.print_available_templates() else: self.logger.error("no template name given, please pick on of the following") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.template_name = args[2] self.logger.info("template_name: %s" % (self.template_name)) def process_final(self): c = self.get_content_by_name(self.template_name) if not c: self.logger.error("not a valid template name %s" % (self.template_name)) return sys.stdout.write(c) class StackTraceMod(Mod): DEFAULT_FILTER = '*.*.*.*:*-*.*.*.*:5001' def initialize(self): self.parse_local_options() sys.stderr.write("# 1. Make sure you have a working systemtap environment\n") sys.stderr.write("# 2. Make sure sudo systemtap ... is working without password (or run as root)\n") sys.stderr.write("# 2. CTRL-C to interrupt data collecting\n") def create_gnuplot_environment(self): gnuplot_filename = "cwnd.gpi" makefile_filename = "Makefile" filepath = "%s/%s" % (self.opts.outputdir, gnuplot_filename) fd = open(filepath, 'w') fd.write("%s" % (Template.cwnd)) fd.close() filepath = "%s/%s" % (self.opts.outputdir, makefile_filename) fd = open(filepath, 'w') fd.write("%s" % (Template.gnuplot_makefile)) fd.close() def check_options(self): if not self.opts.outputdir: self.logger.error("No output directory specified: --output-dir") sys.exit(ExitCodes.EXIT_CMD_LINE) if not os.path.exists(self.opts.outputdir): self.logger.error("Not a valid directory: \"%s\"" % (self.opts.outputdir)) sys.exit(ExitCodes.EXIT_CMD_LINE) def create_data_files(self): self.stap_raw_filepath = "%s/%s" % (self.opts.outputdir, "stap-raw.data") self.stap_raw_file = open(self.stap_raw_filepath, 'w') def close_data_files(self): self.stap_raw_file.close() def parse_local_options(self): self.width = self.height = 0 parser = optparse.OptionParser() parser.usage = "%prog timesequence [options] <pcapfile>" parser.add_option( "-v", "--loglevel", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-o", "--output-dir", dest="outputdir", default=None, type="string", help="specify the output directory") parser.add_option( "-f", "--filter", dest="filter", default=StackTraceMod.DEFAULT_FILTER, type="string", help="specify filter localIP:localPort-remoteIP:remotePort, " + "(default: '*.*.*.*:*-*.*.*.*:5001')") parser.add_option( "-i", "--init", dest="init", default=False, action="store_true", help="create Gnuplot template and Makefile in output-dir") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() self.captcp.print_welcome() self.check_options() if self.opts.init: self.create_gnuplot_environment() self.create_data_files() def process_final(self): stap_script = "%s/data/stap-scripts/tcp-trace.stp" % \ (os.path.dirname(os.path.realpath(__file__))) cmd = [] if os.geteuid() != 0: self.logger.info("execute script via sudo") cmd += ["sudo"] cmd += ["/usr/bin/stap", stap_script, "filter=all", "update=all", self.opts.filter] self.logger.debug("cmd: %s" % (cmd)) sys.stderr.write("# compile and load kernel module, this may take some time ...\n") tsk = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE) poll = select.poll() poll.register(tsk.stdout,select.POLLIN | select.POLLHUP) poll.register(tsk.stderr,select.POLLIN | select.POLLHUP) pollc = 2 events = poll.poll() try: while pollc > 0 and len(events) > 0: for event in events: (rfd,event) = event if event & select.POLLIN: if rfd == tsk.stdout.fileno(): line = tsk.stdout.readline() if len(line) > 0: sys.stdout.write(line) sys.stdout.flush() self.stap_raw_file.write(line) self.stap_raw_file.flush if rfd == tsk.stderr.fileno(): line = tsk.stderr.readline() if len(line) > 0: sys.stderr.write("stap: %s" % line) if event & select.POLLHUP: poll.unregister(rfd) pollc = pollc - 1 if pollc > 0: events = poll.poll() tsk.wait() except KeyboardInterrupt: sys.stderr.write("SIGINT received, flush data files and exit\n") self.close_data_files() sys.stderr.write("# now execute \"make\" in %s\n" % (self.opts.outputdir)) class TimeSequenceMod(Mod): # Gnuplot arrow colors COLOR_ACK_PUSH = "#3333ff" COLOR_DATA_PUSH = "#0000aa" COLOR_ACK_ECE = "#00ff00" COLOR_DATA_ECE = "#00ff00" COLOR_ACK_CWR = "#006600" COLOR_DATA_CWR = "#006600" class Sequence: pass def initialize(self): self.ids = None self.timeframe_start = None self.timeframe_end = None self.reference_time = False self.reference_tx_seq = None self.wscale_sender_support = False self.v_start = None self.v_end = None self.highest_seq = -1 self.wscale_receiver = 1 self.parse_local_options() self.logger.warning("ADVICE: capture the data at sender side!") def create_files(self): self.data_flow_filepath = "%s/%s" % (self.opts.outputdir, "seq.data") self.ack_flow_filepath = "%s/%s" % (self.opts.outputdir, "ack.data") self.receiver_awnd_filepath = "%s/%s" % (self.opts.outputdir, "win.data") self.data_arrow_filepath = "%s/%s" % (self.opts.outputdir, "data-arrow.data") self.data_arrow_retrans_filepath = "%s/%s" % (self.opts.outputdir, "data-arrow-retrans.data") self.data_arrow_sack_filepath = "%s/%s" % (self.opts.outputdir, "data-arrow-sack.data") self.data_arrow_push_filepath = "%s/%s" % (self.opts.outputdir, "data-arrow-push.data") self.data_arrow_ece_filepath = "%s/%s" % (self.opts.outputdir, "data-arrow-ece.data") self.data_arrow_cwr_filepath = "%s/%s" % (self.opts.outputdir, "data-arrow-cwr.data") # line data files self.data_flow_fd = open(self.data_flow_filepath, 'w') self.ack_flow_fd = open(self.ack_flow_filepath, 'w') self.receiver_awnd_fd = open(self.receiver_awnd_filepath, 'w') # required arrow data files self.arrow_data_fd = open(self.data_arrow_filepath, 'w') self.arrow_retr_fd = open(self.data_arrow_retrans_filepath, 'w') self.arrow_sack_fd = open(self.data_arrow_sack_filepath, 'w') # optional (extended) arrow data files if self.opts.extended: self.arrow_push_fd = open(self.data_arrow_push_filepath, 'w') self.arrow_ece_fd = open(self.data_arrow_ece_filepath, 'w') self.arrow_cwr_fd = open(self.data_arrow_cwr_filepath, 'w') def close_files(self): self.data_flow_fd.close() self.ack_flow_fd.close() self.receiver_awnd_fd.close() self.arrow_data_fd.close() self.arrow_retr_fd.close() self.arrow_sack_fd.close() if self.opts.extended: self.arrow_push_fd.close() self.arrow_ece_fd.close() self.arrow_cwr_fd.close() def create_gnuplot_environment(self): gnuplot_filename = "time-sequence.gpi" makefile_filename = "Makefile" gpi_extended_fmt = "load \"data-arrow-push.data\"\n" + \ "load \"data-arrow-ece.data\"\n" + \ "load \"data-arrow-cwr.data\"\n" gpi_hide_window = ", \\\n \"win.data\" using 1:2 " + \ "title \"AWND\" with lines ls 3\n" xrange_str = "" yrange_str = "" if (self.timeframe_start != None) and (self.timeframe_end != None): xrange_str = "set xrange [%s:%s]" % \ (self.timeframe_start, self.timeframe_end) window_sub = gpi_hide_window if self.opts.hide_window: window_sub = "" extended_sub = "" if self.opts.extended: extended_sub = gpi_extended_fmt title='set title "Time Sequence Graph"' if "no-title" in self.opts.gnuplotoptions: title = 'set notitle' if "title" in self.opts.gnuplotoptions: title = "set title \"%s\"" % (self.opts.gnuplotoptions["title"]) logscaley="" if "y-logscale" in self.opts.gnuplotoptions: logscaley = "set logscale y" logscalex="" if "x-logscale" in self.opts.gnuplotoptions: logscalex = "set logscale x" size_ratio="" if "size-ratio" in self.opts.gnuplotoptions: size_ratio = "set size ratio %.2f" % (self.opts.gnuplotoptions["size-ratio"]) # Normal format or extended format tmpl = string.Template(TemplateMod().get_content_by_name("time-sequence")) gpi_cmd = tmpl.substitute(EXTENDED=extended_sub, HIDEWINDOW=window_sub, XRANGE=xrange_str, TITLE=title, LOGSCALEY=logscaley, LOGSCALEX=logscalex, SIZE_RATIO=size_ratio, YRANGE="") filepath = "%s/%s" % (self.opts.outputdir, gnuplot_filename) fd = open(filepath, 'w') fd.write("%s" % (gpi_cmd)) fd.close() filepath = "%s/%s" % (self.opts.outputdir, makefile_filename) fd = open(filepath, 'w') fd.write("%s" % (TemplateMod().get_content_by_name("gnuplot"))) fd.close() def check_options(self): if not self.opts.outputdir: self.logger.error("No output directory specified: --output-dir") sys.exit(ExitCodes.EXIT_CMD_LINE) if not os.path.exists(self.opts.outputdir): self.logger.error("Not a valid directory: \"%s\"" % (self.opts.outputdir)) sys.exit(ExitCodes.EXIT_CMD_LINE) if self.opts.timeframe: (start, end) = self.opts.timeframe.split(':') (self.timeframe_start, self.timeframe_end) = \ (float(start), float(end)) self.logger.debug("split timeframe options: %f - %f" % (self.timeframe_start, self.timeframe_end)) if self.opts.init: self.create_gnuplot_environment() if not self.opts.connections: self.logger.error("No data flow specified (where the data flows)") sys.exit(ExitCodes.EXIT_CMD_LINE) if self.opts.zero and not numpy: sys.logger.error("Python numpy module not installed - but required\n") sys.exit(ExitCodes.EXIT_CMD_LINE) (self.connection_id, self.data_flow_id) = self.opts.connections.split('.') if int(self.data_flow_id) == 1: self.ack_flow_id = 2 elif int(self.data_flow_id) == 2: self.ack_flow_id = 1 else: raise ArgumentException("sub flow must be 1 or 2") sys.stderr.write("# connection: %s (data flow: %s, ACK flow: %s)\n" % (self.connection_id, self.data_flow_id, self.ack_flow_id)) def prepare_gnuplot_options(self): if not self.opts.gnuplotoptions: self.opts.gnuplotoptions = dict() return options = self.opts.gnuplotoptions.split(',') self.opts.gnuplotoptions = dict() for option in options: if option == "no-title" or option == "notitle": self.opts.gnuplotoptions["no-title"] = True continue if option == "x-logscale": self.opts.gnuplotoptions["x-logscale"] = True continue if option == "y-logscale": self.opts.gnuplotoptions["y-logscale"] = True continue if option.startswith("title="): title_token = option.split('=') if len(title_token) != 2: raise ArgumentException("Gnuplot title must be in " "form: title=\"New Title\"") self.opts.gnuplotoptions["title"] = title_token[1] continue if option.startswith("size-ratio="): title_token = option.split('=') if len(title_token) != 2: raise ArgumentException("Gnuplot size-ratio must be in " "form: size-ration=0.4") self.opts.gnuplotoptions["size-ratio"] = float(title_token[1]) if (not self.opts.gnuplotoptions["size-ratio"] > 0.0 and not self.opts.gnuplotoptions["size-ratio"] < 1.0): raise ArgumentException("Gnuplot size-ratio must be in " "range 0.01 to 0.99") continue # unknown options, raise error raise ArgumentException("Unknown gnuplot option: %s" % (option)) # sanity check if ("no-title" in self.opts.gnuplotoptions and "title" in self.opts.gnuplotoptions): raise ArgumentException("Gnuplot title AND no-title options are not allowed") def parse_local_options(self): self.width = self.height = 0 parser = optparse.OptionParser() parser.usage = "%prog timesequence [options] <pcapfile>" parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (debug, info, warning, error)") parser.add_option( "-o", "--output-dir", dest="outputdir", default=None, type="string", help="specify the output directory") parser.add_option( "-f", "--data-flow", dest="connections", default=None, type="string", help="specify the number of relevant ID's") parser.add_option( "-t", "--time", dest="timeframe", default=None, type="string", help="select range of displayed packet (-t <start:stop>)") parser.add_option( "-i", "--init", dest="init", default=False, action="store_true", help="create Gnuplot template and Makefile in output-dir") parser.add_option( "-z", "--zero", dest="zero", default=False, action="store_true", help="start with TCP sequence number 0") parser.add_option( "-e", "--extended", dest="extended", default=False, action="store_true", help="visualize PUSH and ECE/CWR(ECN) bits too") parser.add_option( "-w", "--hide-window", dest="hide_window", default=False, action="store_true", help="do not visualize advertised window") parser.add_option( "-g", "--gnuplot-options", dest="gnuplotoptions", default=None, type="string", help="options for gnuplot, comma separated list: notitle, title=\"<newtitle>\", x-logarithmic, y-logarithmic, size-ratio=<float>") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: self.logger.critical("No pcap file argument given, exiting\n") parser.print_help(sys.stderr) sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.prepare_gnuplot_options() self.check_options() self.captcp.pcap_file_path = args[2] self.logger.info("pcap file: %s" % (self.captcp.pcap_file_path)) self.create_files() def check_packet(self, ts, packet): if type(packet) != dpkt.ip.IP and type(packet) != dpkt.ip6.IP6: return False if type(packet.data) != dpkt.tcp.TCP: return False if not self.cc.is_packet_connection(packet, int(self.connection_id)): return False if not self.reference_time: self.reference_time = ts packet_time = float(self.calculate_offset_time(ts)) if not self.v_start: self.v_start = packet_time if not self.v_end: self.v_end = packet_time self.v_end = max(packet_time, self.v_end) return True def pre_process_data_flow_packet(self, ts, packet): packet_time = self.calculate_offset_time(ts) if not self.reference_tx_seq: self.reference_tx_seq = TcpPacketInfo(packet).seq; def pre_process_packet(self, ts, packet): if not self.check_packet(ts, packet): return sub_connection = self.cc.sub_connection_by_packet(packet) if sub_connection.sub_connection_id == int(self.data_flow_id): self.pre_process_data_flow_packet(ts, packet) def pre_process_final(self): self.logger.info("displayed time frame: %.2fs to %.2fs" % (self.v_start, self.v_end)) self.arrow_length = (self.v_end - self.v_start) * 0.025 self.logger.debug("arrow length in seconds: %lf" % (self.arrow_length)) def calculate_offset_time(self, ts): time_diff = ts - self.reference_time return float(time_diff.seconds) + time_diff.microseconds / \ 1E6 + time_diff.days * 86400 def process_data_flow_packet(self, ts, packet): packet_time = self.calculate_offset_time(ts) pi = TcpPacketInfo(packet) sequence_number = int(pi.seq) if self.opts.zero: sequence_number = SequenceContainer.uint32_sub(sequence_number, self.reference_tx_seq) self.data_flow_fd.write("%lf %u\n" % (packet_time, sequence_number)) # support the sender wscale? if pi.options['wsc']: self.wscale_sender_support = True # visualize PUSH flag if self.opts.extended: if pi.is_psh_flag(): self.arrow_push_fd.write( "set arrow from %lf,%s.0 to %ls,%s.0 front lc rgb \"%s\" lw 2\n" % (packet_time - self.arrow_length, sequence_number, packet_time, sequence_number, TimeSequenceMod.COLOR_DATA_PUSH)) if pi.is_ece_flag(): self.arrow_ece_fd.write( "set arrow from %lf,%s.0 to %ls,%s.0 front lc rgb \"%s\" lw 2\n" % (packet_time + self.arrow_length, sequence_number, packet_time, sequence_number, TimeSequenceMod.COLOR_DATA_ECE)) if pi.is_cwr_flag(): # CRW get a offset of "2 bytes" to show CWR and ECE self.arrow_cwr_fd.write( "set arrow from %lf,%s.0 to %ls,%s.0 front lc rgb \"%s\" lw 2\n" % (packet_time + self.arrow_length, sequence_number + 2, packet_time, sequence_number + 2, TimeSequenceMod.COLOR_DATA_CWR)) # differentiate between new data send # or already sent data (thus retransmissins) if pi.seq > self.highest_seq: self.arrow_data_fd.write( "set arrow from %lf,%s.0 to %ls,%s.0 front lc rgb \"#008800\" lw 1\n" % (packet_time, sequence_number, packet_time, sequence_number + len(packet.data.data))) else: self.arrow_retr_fd.write( "set arrow from %lf,%s.0 to %ls,%s.0 front lc rgb \"red\" lw 1\n" % (packet_time, sequence_number, packet_time, sequence_number + len(packet.data.data))) # only real data packets should be accounted, no plain ACKs if len(packet.data.data) > 0: self.highest_seq = max(pi.seq, self.highest_seq) def calc_advertised_window(self, pi): # only enabled if both hosts support window scaling if not self.wscale_sender_support: return pi.win + pi.ack else: return pi.win * self.wscale_receiver + pi.ack def process_ack_flow_packet(self, ts, packet): packet_time = self.calculate_offset_time(ts) pi = TcpPacketInfo(packet) # ignore first ACK packet if pi.ack == 0: return if self.opts.zero: pi.ack = SequenceContainer.uint32_sub(int(pi.ack), self.reference_tx_seq) # write ACK number self.ack_flow_fd.write("%lf %d\n" % (packet_time, pi.ack)) # visualize PUSH flag if self.opts.extended: if pi.is_psh_flag(): self.arrow_push_fd.write( "set arrow from %lf,%s.0 to %ls,%s.0 front lc rgb \"%s\" lw 2\n" % (packet_time - self.arrow_length, pi.ack, packet_time, pi.ack, TimeSequenceMod.COLOR_ACK_PUSH)) if pi.is_ece_flag(): self.arrow_ece_fd.write( "set arrow from %lf,%s.0 to %ls,%s.0 front lc rgb \"%s\" lw 2\n" % (packet_time + self.arrow_length, pi.ack, packet_time, pi.ack, TimeSequenceMod.COLOR_ACK_ECE)) if pi.is_cwr_flag(): # CRW get a offset of "2 bytes" to show CWR and ECE self.arrow_cwr_fd.write( "set arrow from %lf,%s.0 to %ls,%s.0 front lc rgb \"%s\" lw 2\n" % (packet_time + self.arrow_length, pi.ack + 2, packet_time, pi.ack + 2, TimeSequenceMod.COLOR_ACK_CWR)) # write advertised window if not explicit disabled if not self.opts.hide_window: self.receiver_awnd_fd.write("%lf %s\n" % (packet_time, self.calc_advertised_window(pi))) if pi.options['sackblocks']: for i in range(len(pi.options['sackblocks'])): sack_start = int(pi.options['sackblocks'][i][0]) sack_end = int(pi.options['sackblocks'][i][1]) if self.opts.zero: sack_start = SequenceContainer.uint32_sub(sack_start, self.reference_tx_seq) sack_end = SequenceContainer.uint32_sub(sack_end, self.reference_tx_seq) self.arrow_sack_fd.write( "set arrow from %lf,%s.0 to %ls,%s.0 nohead front lc rgb \"#aaaaff\" lw 2\n" % (packet_time, sack_start, packet_time, sack_end)) # we set self.wscale_receiver at the end to bypass # the first SYN/ACK packet where a) the window option # is transmitted. This window MUST not be scaled. if pi.options['wsc']: self.wscale_receiver = math.pow(2, int(pi.options['wsc'])) def process_packet(self, ts, packet): if not self.check_packet(ts, packet): return sub_connection = self.cc.sub_connection_by_packet(packet) if sub_connection.sub_connection_id == int(self.data_flow_id): self.process_data_flow_packet(ts, packet) elif sub_connection.sub_connection_id == int(self.ack_flow_id): self.process_ack_flow_packet(ts, packet) else: raise InternalException def process_final(self): self.close_files() self.logger.warning("now execute (cd %s; make preview)" % (self.opts.outputdir)) class FlowGraphMod(Mod): class Sequence: pass # more then PACKET_THRESH_FEW packets and the axis labeling # is reduced PACKET_LABELING_THRESH = 100 def initialize(self): self.ids = None self.timeframe_start = self.timeframe_end = False if cairo == None: raise ImportError("Python Cairo module not available, exiting") self.parse_local_options() self.setup_cairo() self.tracing_start = None self.tracing_end = None self.process_time_start = self.process_time_end = None self.packets_to_draw = 0 self.packet_timestamp_punchcard = dict() self.packet_timestamp_punchcard[True] = [] self.packet_timestamp_punchcard[False] = [] self.reference_time = False def setup_cairo(self): line_width = 1.6 surface = cairo.PDFSurface(self.opts.filename, self.width, self.height) self.cr = cairo.Context(surface) self.cr.move_to(0, 0) self.cr.set_source_rgb(1.0, 1.0, 1.0) # white self.cr.rectangle(0, 0, self.width, self.height) self.cr.fill() self.cr.stroke() self.margin_left_right = 50 self.margin_top_bottom = 50 # left self.cr.move_to(self.margin_left_right, self.margin_top_bottom) self.cr.line_to (self.margin_left_right, self.height - self.margin_top_bottom) self.cr.set_source_rgb(0.0, 0.0, 0.0) self.cr.set_line_width(line_width) self.cr.stroke() text = self.opts.locallabel self.cr.set_font_size(12) x_bearing, y_bearing, width, height = self.cr.text_extents(text)[:4] x_off = self.margin_left_right - (width / 2) y_off = self.margin_top_bottom - (height) - 10 self.cr.move_to(x_off, y_off) self.cr.show_text(text) self.cr.stroke() # right self.cr.move_to(self.width - self.margin_left_right, self.margin_top_bottom) self.cr.line_to(self.width - self.margin_left_right, self.height - self.margin_top_bottom) self.cr.set_line_width(line_width) self.cr.stroke() text = self.opts.remotelabel self.cr.set_font_size(12) x_bearing, y_bearing, width, height = self.cr.text_extents(text)[:4] x_off = self.width - self.margin_left_right - (width / 2) y_off = self.margin_top_bottom - (height) - 10 self.cr.move_to(x_off, y_off) self.cr.show_text(text) self.cr.stroke() def draw_background_grid(self): grid_line_width = 0.1 left = self.margin_left_right right = self.width - self.margin_left_right i = self.margin_top_bottom while True: self.cr.move_to(left, i) self.cr.line_to (right, i) self.cr.set_source_rgb(0.9, 0.9, 0.9) self.cr.set_line_width(grid_line_width) self.cr.stroke() i += 20 if i > self.height - self.margin_top_bottom: break def pre_process_final(self): if self.ids: time_start = time_end = None for idss in self.ids: conn = self.cc.connection_by_uid(int(idss)) if conn: if time_start == None: time_start = conn.capture_time_start if time_end == None: time_end = conn.capture_time_end if time_start > conn.capture_time_start: time_start = conn.capture_time_start if time_end < conn.capture_time_end: time_end = conn.capture_time_end time_diff = time_end - time_start time_diff = float(time_diff.seconds) + time_diff.microseconds / \ 1E6 + time_diff.days * 86400 time_diff += self.delay self.scaling_factor = time_diff / (self.height - 2 * self.margin_top_bottom) self.process_time_start = time_start self.process_time_end = time_end else: if self.timeframe_start and self.timeframe_end: self.logger.debug("calculate page coordinated and scale factor") timedelta_s = datetime.timedelta(seconds=self.timeframe_start) timedelta_e = datetime.timedelta(seconds=self.timeframe_end) self.capture_time_start = self.reference_time + timedelta_s self.capture_time_end = self.reference_time + timedelta_e self.process_time_start = self.capture_time_start self.process_time_end = self.capture_time_end time_diff = self.process_time_end - self.process_time_start time_diff = float(time_diff.seconds) + \ time_diff.microseconds / 1E6 + time_diff.days * 86400 time_diff += self.delay self.scaling_factor = time_diff / \ (self.height - 2 * self.margin_top_bottom) else: # this is the _normal_ case: the user didn't specified # a connection id nor a timestart, timeend limit time_diff = self.cc.capture_time_end - self.cc.capture_time_start time_diff = float(time_diff.seconds) + \ time_diff.microseconds / 1E6 + time_diff.days * 86400 time_diff += self.delay self.scaling_factor = time_diff / (self.height - 2 * self.margin_top_bottom) if self.opts.grid: self.draw_background_grid() self.logger.info("now draw %d packets" % self.packets_to_draw) def rttbw_to_bits(self): if "mbps" in self.opts.rttbw.lower(): return float(self.opts.rttbw.lower().replace("mbps",""))*1000000 if "kbps" in self.opts.rttbw.lower(): return float(self.opts.rttbw.lower().replace("kbps",""))*1000 if "bps" in self.opts.rttbw.lower(): return float(self.opts.rttbw.lower().replace("bps","")) def parse_local_options(self): self.width = self.height = 0 parser = optparse.OptionParser() parser.usage = "%prog flowgraph [options] <pcapfile>" parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-l", "--local", dest="localaddr", default=None, type="string", help="specify list of local ip addresses") parser.add_option( "-s", "--size", dest="size", default="600x1200", type="string", help="specify the size of the image (default: 600x1200)") parser.add_option( "-r", "--rtt", dest="rtt", default=0.025, type="float", help="specify the average rtt per connection (default 0.025s)") parser.add_option( "-b", "--rtt-bw", dest="rttbw", default="100Mbps", type="string", help="specify the used bandwidth for your connection (default 100Mbps)") parser.add_option( "-f", "--filename", dest="filename", default="seq-graph.pdf", type="string", help="specify the name of the generated PDF file (default: seq-graph.pdf)") parser.add_option( "-i", "--connection-id", dest="connections", default=None, type="string", help="specify the number of relevant ID's") parser.add_option( "-y", "--style", dest="style", default="normal", type="string", help="specify the style of the labels (normal or minimal)") parser.add_option( "-p", "--locallabel", dest="locallabel", default="Local", type="string", help="the default string left axis (default: Local)") parser.add_option( "-q", "--remotelabel", dest="remotelabel", default="Remote", type="string", help="the default string right axis (default: Remote)") parser.add_option( "-g", "--grid", dest="grid", default=False, type="string", help="draw background grid (default: no)") parser.add_option( "-t", "--time", dest="timeframe", default=None, type="string", help="select range of displayed packet (-t <start:stop>)") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: parser.error("no pcap file argument given, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() if self.opts.localaddr == None: self.logger.warning("No local IP address (--local <ip-addr>) specified!") if self.opts.timeframe: self.logger.debug("split timeframe options: %s" % (self.opts.timeframe)) (start, end) = self.opts.timeframe.split(':') (self.timeframe_start, self.timeframe_end) = (float(start), float(end)) self.logger.debug("%s %s" %(self.timeframe_start, self.timeframe_end)) self.captcp.pcap_file_path = args[2] self.logger.info("pcap file: %s" % (self.captcp.pcap_file_path)) (self.width, self.height) = self.opts.size.split('x') (self.width, self.height) = (int(self.width), int(self.height)) self.delay = self.opts.rtt / 2.0 self.bandwidth = self.rttbw_to_bits() if self.width <= 0 or self.height <= 0: raise ArgumentException("size cannot smaller then 0px") if self.opts.connections: self.ids = self.opts.connections.split(',') self.logger.info("visualization limited to the following connections: %s" % (str(self.ids))) def local_generated_packet(self, packet): if type(packet) == dpkt.ip.IP: if Converter.dpkt_addr_to_string(packet.src) == self.opts.localaddr: return True else: return False elif type(packet) == dpkt.ip6.IP6: ipv6str = socket.inet_ntop(socket.AF_INET6, packet.src) if ipv6str == self.opts.localaddr: return True else: return False else: raise InternalException() def ts_tofloat(self, ts): return float(ts.seconds) + ts.microseconds / 1E6 + ts.days * 86400 def reduced_labeling(self): return self.packets_to_draw > FlowGraphMod.PACKET_LABELING_THRESH def draw_timestamp(self, sequence, ts, packet): time = "%.5f" % (ts) if sequence.local: x_offset = 5 else: x_offset = self.width - self.margin_left_right + 5 self.cr.set_font_size(6) # draw test x_bearing, y_bearing, width, height = self.cr.text_extents(time)[:4] y_offset = sequence.ys + (height / 2) # we check that there is no label somewhere in range for val in self.packet_timestamp_punchcard[sequence.local]: if (y_offset - height - 1) < val and (y_offset + height + 1) > val: # skip drawing return self.cr.move_to(x_offset, y_offset) self.cr.show_text(time) self.cr.stroke() self.packet_timestamp_punchcard[sequence.local].append(y_offset) def construct_label_string(self, packet): pi = TcpPacketInfo(packet) if self.opts.style == "minimal": text = "%s seq:%u ack:%u len: %u" % ( pi.create_flag_brakets(), pi.seq, pi.ack, len(packet.data.data)) else: # including "normal" style text = "%s seq:%u ack:%u win:%u urp:%u" % ( pi.create_flag_brakets(), pi.seq, pi.ack, pi.win, pi.urp) text += " {" if pi.options['mss']: text += " mss: %d" % (pi.options['mss']) if pi.options['wsc']: text += " wsc: %d" % (pi.options['wsc']) if pi.options['tsval']: text += " tsval: %d" % (pi.options['tsval']) if pi.options['sackok']: text += " sackok" text += "}" return text def draw_labels(self, sequence, ts, packet): local_margin = 3 gegenkathete = sequence.ye - sequence.ys ankathete = self.width - (self.margin_left_right * 2) res = math.atan(gegenkathete/ankathete) if not sequence.local: res = (math.pi * 2) - res text = self.construct_label_string(packet) self.cr.save() self.cr.set_font_size(9) x_bearing, y_bearing, width, height = self.cr.text_extents(text)[:4] x_off = (self.width / 2) - (width / 2.0) mid = (sequence.ys + (sequence.ye - sequence.ys) / 2.0) if sequence.local: gk = math.atan(res) * (width / 2.0) y_off = (mid) - (height / 2.0) - gk + local_margin else: res = math.atan(gegenkathete/ankathete) gk = math.atan(res) * (width / 2.0) if not sequence.local: res = (math.pi * 2) - res y_off = (mid) - (height / 2.0) + gk + local_margin self.cr.move_to(x_off, y_off) self.cr.rotate(res) self.cr.show_text(text) self.cr.stroke() self.cr.restore() def draw_sequence(self, sequence, ts, packet): self.draw_timestamp(sequence, ts, packet) if not self.reduced_labeling(): self.draw_labels(sequence, ts, packet) self.cr.set_line_width(0.5) self.cr.move_to(sequence.xs, sequence.ys) self.cr.line_to(sequence.xe, sequence.ye) self.cr.stroke() def draw_arrows(self, sequence): pi_half = math.pi / 2.0 if sequence.xe-sequence.xs > 0 and sequence.ye-sequence.ys > 0: interim_angle = pi_half - (math.atan((sequence.ye - sequence.ys) / \ (sequence.xe-sequence.xs))) - 0.4 xsp = sequence.xe - (6 * math.sin(interim_angle)) ysp = sequence.ye - (6 * math.cos(interim_angle)) interim_angle = pi_half - (math.atan((sequence.xe - sequence.xs) / \ (sequence.ye - sequence.ys))) - 0.4 xep = sequence.xe - (6 * math.cos(interim_angle)) yep = sequence.ye - (6 * math.sin(interim_angle)) if sequence.xe - sequence.xs > 0 and sequence.ye - sequence.ys < 0: interim_angle= pi_half - (math.atan((sequence.xe - sequence.xs) / \ (sequence.ys - sequence.ye))) - 0.4 xsp = sequence.xe - (6 * math.cos(interim_angle)) ysp = sequence.ye + (6 * math.sin(interim_angle)) interim_angle = pi_half - (math.atan((sequence.ys - sequence.ye) / \ (sequence.ye - sequence.ys))) - 0.4 xep = sequence.xe - (6 * math.sin(interim_angle)) yep = sequence.ye + (6 * math.cos(interim_angle)) if sequence.xe - sequence.xs < 0 and sequence.ye - sequence.ys < 0: interim_angle = pi_half - (math.atan((sequence.ys-sequence.ye) / \ (sequence.xs - sequence.xe))) - 0.4 xsp = sequence.xe + (6 * math.sin(interim_angle)) ysp = sequence.ye + (6 * math.cos(interim_angle)) interim_angle = pi_half - (math.atan((sequence.xs - sequence.xe) / \ (sequence.ys-sequence.ye))) - 0.4 xep = sequence.xe + (6 * math.cos(interim_angle)) yep = sequence.ye + (6 * math.sin(interim_angle)) if sequence.xe - sequence.xs < 0 and sequence.ye - sequence.ys > 0: interim_angle = pi_half - (math.atan((sequence.ye - sequence.ys) / \ (sequence.xs - sequence.xe))) - 0.4 xsp = sequence.xe + ( 6 *math.sin(interim_angle)) ysp = sequence.ye - ( 6 *math.cos(interim_angle)) interim_angle = pi_half - (math.atan((sequence.xs - sequence.xe) / \ (sequence.ye-sequence.ys))) - 0.4 xep = sequence.xe + (6 * math.cos(interim_angle)) yep = sequence.ye - (6 * math.sin(interim_angle)) if xsp and ysp: self.cr.set_source_rgb(0.0,0.0,0.0) self.cr.set_line_width(0.5) self.cr.move_to(sequence.xe, sequence.ye) self.cr.line_to(xsp, ysp) self.cr.line_to(xep, yep) self.cr.line_to(sequence.xe, sequence.ye) self.cr.close_path() self.cr.fill() def is_drawable_packet(self, ts, packet): if type(packet) != dpkt.ip.IP and type(packet) != dpkt.ip6.IP6: return False if type(packet.data) != dpkt.tcp.TCP: return False if self.process_time_start and ts < self.process_time_start: return False if self.process_time_end and ts > self.process_time_end: return False if self.ids: for idss in self.ids: if self.cc.is_packet_connection(packet, int(idss)): return True else: return True return False def pre_process_packet(self, ts, packet): if not self.reference_time: # time time where the first packet is # captured self.reference_time = ts if not self.is_drawable_packet(ts, packet): return self.packets_to_draw += 1 def process_packet(self, ts, packet): if not self.is_drawable_packet(ts, packet): return if self.process_time_start: ts_diff = ts - self.process_time_start else: ts_diff = ts - self.cc.capture_time_start s = FlowGraphMod.Sequence if self.local_generated_packet(packet): s.local = True s.xs = self.margin_left_right s.xe = self.width - self.margin_left_right s.ys = self.ts_tofloat(ts_diff) / self.scaling_factor s.ye = (self.ts_tofloat(ts_diff) + \ (len(packet)/self.bandwidth) + self.delay) / self.scaling_factor display_time = self.ts_tofloat(ts_diff) else: s.local = False s.xs = self.width - self.margin_left_right s.xe = self.margin_left_right s.ys = (self.ts_tofloat(ts_diff) - \ (len(packet)/self.bandwidth) - self.delay) /self.scaling_factor s.ye = self.ts_tofloat(ts_diff) / self.scaling_factor display_time = self.ts_tofloat(ts_diff) - (len(packet)/self.bandwidth) - self.delay s.ys += self.margin_top_bottom s.ye += self.margin_top_bottom self.cr.set_source_rgb(0.0, 0.0, 0.0) self.draw_sequence(s, display_time, packet) self.draw_arrows(s) def process_final(self): self.logger.debug("generate PDF file \"%s\"" % (self.opts.filename)) self.cr.show_page() class TcpConn: def __init__(self, packet): ip = packet tcp = packet.data self.ipversion = str(type(ip)) self.sip = Converter.dpkt_addr_to_string(ip.src) self.dip = Converter.dpkt_addr_to_string(ip.dst) self.sport = str(int(tcp.sport)) self.dport = str(int(tcp.dport)) self.sipnum = ip.src self.dipnum = ip.dst l = [ord(a) ^ ord(b) for a,b in zip(self.sipnum, self.dipnum)] self.uid = "%s:%s:%s" % ( str(self.ipversion), str(l), str(long(self.sport) + long(self.dport))) self.iuid = ((self.sipnum) + \ (self.dipnum) + ((self.sport) + \ (self.dport))) def __hash__(self): return self.iuid def __repr__(self): return "%s:%s<->%s:%s" % ( self.sip, self.sport, self.dip, self.dport) class SubConnectionStatistic: def __init__(self): self.packets_processed = 0 self.bytes_sent_link_layer = 0 self.bytes_sent_network_layer = 0 self.bytes_sent_transport_layer = 0 self.bytes_sent_application_layer = 0 class SubConnection(TcpConn): def __init__(self, connection, packet): TcpConn.__init__(self, packet) self.connection = connection self.statistic = SubConnectionStatistic() self.user_data = dict() def __cmp__(self, other): if other == None: return True if (self.dipnum == other.dipnum and self.sipnum == other.sipnum and self.dport == other.dport and self.sport == other.sport and self.ipversion == other.ipversion): return False else: return True def __repr__(self): return "%s:%s -> %s:%s" % ( self.sip, self.sport, self.dip, self.dport) def update(self, ts, packet): self.statistic.packets_processed += 1 def set_subconnection_id(self, sub_connection_id): self.sub_connection_id = sub_connection_id def is_in(self, ids): for i in ids: if i.find('.') != -1: assert(i.count('.') == 1) (major, minor) = i.split('.') if (int(major) == self.connection.connection_id and int(minor) == self.sub_connection_id): return True else: if int(i) == self.connection.connection_id: return True return False class ConnectionStatistic: def __init__(self): self.packets_processed = 0 self.bytes_sent_link_layer = 0 self.bytes_sent_network_layer = 0 self.bytes_sent_transport_layer = 0 self.bytes_sent_application_layer = 0 class Connection(TcpConn): static_connection_id = 1 def __init__(self, packet): TcpConn.__init__(self, packet) (self.sc1, self.sc2) = (None, None) self.connection_id = Connection.static_connection_id Connection.static_connection_id += 1 self.statistic = ConnectionStatistic() self.capture_time_start = None self.capture_time_end = None # module users could use this container # to stick data to a connection self.user_data = dict() def __del__(self): Connection.static_connection_id -= 1 def __cmp__(self, other): if self.ipversion != other.ipversion: return False if (self.dipnum == other.dipnum and self.sipnum == other.sipnum and self.dport == other.dport and self.sport == other.sport): return True elif (self.dipnum == other.sipnum and self.sipnum == other.dipnum and self.dport == other.sport and self.sport == other.dport): return True else: return False def register_container(self, container): self.container = container def update_statistic(self, packet): self.statistic.packets_processed += 1 def update(self, ts, packet): self.update_statistic(packet) sc = SubConnection(self, packet) if self.capture_time_start == None: self.capture_time_start = ts self.capture_time_end = ts if self.sc1 == None: self.sc1 = sc self.sc1.update(ts, packet) self.sc1.set_subconnection_id(1) return if self.sc1 == sc: self.sc1.update(ts, packet) return if self.sc2 == sc: self.sc2.update(ts, packet) return self.sc2 = sc sc.update(ts, packet) sc.set_subconnection_id(2) def get_subconnection(self, packet): # we know that packet is a TCP packet if self.sc1 == None: raise InternalException("a connection without a subconnection?!") if str(self.sc1.sport) == str(packet.data.sport): return self.sc1 else: assert(self.sc2) return self.sc2 class ConnectionContainerStatistic: def __init__(self): self.packets_processed = 0 self.packets_nl_arp = 0 self.packets_nl_ipv4 = 0 self.packets_nl_ipv6 = 0 self.packets_nl_unknown = 0 self.packets_tl_tcp = 0 self.packets_tl_udp = 0 self.packets_tl_icmp = 0 self.packets_tl_icmp6 = 0 self.packets_tl_unknown = 0 # byte accounting self.bytes_sent_link_layer = 0 self.bytes_sent_network_layer = 0 self.bytes_sent_transport_layer = 0 class ConnectionContainer: def __init__(self): self.container = dict() self.statistic = ConnectionContainerStatistic() self.capture_time_start = None self.capture_time_end = None def __len__(self): return len(self.container) def connection_by_uid(self, uid): for i in self.container.keys(): if self.container[i].connection_id == uid: return self.container[i] return None def tcp_check(self, packet): if type(packet) != dpkt.ip.IP and type(packet) != dpkt.ip6.IP6: return False if type(packet.data) != dpkt.tcp.TCP: return False return True def sub_connection_by_packet(self, packet): if not self.tcp_check(packet): return None c = Connection(packet) if not c.uid in self.container.keys(): # this method SHOULD not be called if not # sure that the packet is already in the # container raise InternalException("packet MUST be in preprocesses container") return self.container[c.uid].get_subconnection(packet) def connection_by_packet(self, packet): if not self.tcp_check(packet): return None c = Connection(packet) if not c.uid in self.container.keys(): raise InternalException("packet MUST be in preprocesses container") else: return self.container[c.uid] def is_packet_connection(self, packet, uid): if not self.tcp_check(packet): return None c = Connection(packet) if not c.uid in self.container.keys(): raise InternalException("packet MUST be in preprocesses container") else: cc = self.container[c.uid] if cc.connection_id == uid: return True else: return False def update(self, ts, packet): if type(packet) != dpkt.ip.IP and type(packet) != dpkt.ip6.IP6: return if type(packet.data) != dpkt.tcp.TCP: return if self.capture_time_start == None: self.capture_time_start = ts self.capture_time_end = ts c = Connection(packet) # this is the only place where a connetion # is put into this container if not c.uid in self.container.keys(): c.update(ts, packet) self.container[c.uid] = c c.register_container(self) else: cc = self.container[c.uid] cc.update(ts, packet) class ConnectionAnalyzeMod(Mod): def initialize(self): parser = optparse.OptionParser() parser.usage = "captcp connection" parser.add_option( "-v", "--loglevel", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: self.logger.error("no pcap file argument given, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.pcap_file_path = args[2] self.captcp.pcap_filter = None if args[3:]: self.captcp.pcap_filter = " ".join(args[3:]) self.logger.info("pcap filter: \"" + self.captcp.pcap_filter + "\"") def process_final(self): sys.stdout.write("digraph G {\nranksep=3.0;\nnodesep=2.0;\n") sys.stdout.write("#size=\"7.75,10.25\";\n orientation=\"landscape\"\n") label = "\"%d\" [ label=\"%s\",style=filled,color=none,fontsize=6,fontname=Helvetica];\n" sys.stdout.write(label % (1, str("Connections"))) for key in self.cc.container.keys(): connection = self.cc.container[key] sys.stdout.write(label % (abs(hash(connection.iuid)), str(connection))) if connection.sc1 and connection.sc2: sys.stdout.write(label % (abs(hash(connection.sc1.iuid) + 1), str(connection.sc1))) sys.stdout.write(label % (abs(hash(connection.sc2.iuid) + 2), str(connection.sc2))) elif connection.sc1: sys.stdout.write(label % (abs(hash(connection.sc1.iuid) + 1), str(connection.sc1))) # connect label = "\"%s\" -> \"%s\" [ label=\" \",color=gray,arrowsize=0.4, penwidth=1.2 ];\n" for key in self.cc.container.keys(): connection = self.cc.container[key] sys.stdout.write(label % (1, (abs(hash(connection.iuid))))) if connection.sc1 and connection.sc2: sys.stdout.write(label % ((abs(hash(connection.iuid))), (abs(hash(connection.sc1.iuid) + 1)))) sys.stdout.write(label % ((abs(hash(connection.iuid))), (abs(hash(connection.sc2.iuid) + 2)))) elif connection.sc1: sys.stdout.write(label % ((abs(hash(connection.iuid))), (abs(hash(connection.sc1.iuid) + 1)))) sys.stdout.write("}\n") sys.stderr.write("# Tip: generate graphviz file with: \"twopi -onetwork.png -Tpng network.data\"\n") class ThroughputMod(Mod): def create_gnuplot_environment(self): gnuplot_filename = "throughput.gpi" makefile_filename = "Makefile" title = "set title \"Throughput Graph\"" if "no-title" in self.opts.gnuplotoptions: title = 'set notitle' if "title" in self.opts.gnuplotoptions: title = "set title \"%s\"" % (self.opts.gnuplotoptions["title"]) if self.opts.persecond: unit = self.opts.unit + "/s" else: unit = self.opts.unit # throughput.gpi tmpl = string.Template(TemplateMod().get_content_by_name("throughput")) gpi_cmd = tmpl.substitute(TITLE=title, UNIT=unit) filepath = "%s/%s" % (self.opts.outputdir, gnuplot_filename) fd = open(filepath, 'w') fd.write("%s" % (gpi_cmd)) fd.close() # Makefile filepath = "%s/%s" % (self.opts.outputdir, makefile_filename) fd = open(filepath, 'w') fd.write("%s" % (TemplateMod().get_content_by_name("gnuplot"))) fd.close() def check_options(self): if not self.opts.outputdir: self.logger.error("No output directory specified: --output-dir") sys.exit(ExitCodes.EXIT_CMD_LINE) if not os.path.exists(self.opts.outputdir): self.logger.error("Not a valid directory: \"%s\"" % (self.opts.outputdir)) sys.exit(ExitCodes.EXIT_CMD_LINE) def create_data_files(self): self.throughput_filepath = \ "%s/%s" % (self.opts.outputdir, "throughput.data") self.throughput_file = open(self.throughput_filepath, 'w') def close_data_files(self): self.throughput_file.close() def initialize(self): self.parse_local_options() self.end_time = self.start_time = False self.total_data_len = 0 self.time_clipping_delta = 0.0 self.first_packet_seen = None if not self.opts.stdio: # no need to check and generate Gnuplot # environment self.check_options() if self.opts.init: self.create_gnuplot_environment() self.create_data_files() def prepare_gnuplot_options(self): if not self.opts.gnuplotoptions: self.opts.gnuplotoptions = dict() return options = self.opts.gnuplotoptions.split(',') self.opts.gnuplotoptions = dict() for option in options: if option == "no-title" or option == "notitle": self.opts.gnuplotoptions["no-title"] = True continue if option.startswith("title="): title_token = option.split('=') if len(title_token) != 2: raise ArgumentException("Gnuplot title must be in " "form: title=\"New Title\"") self.opts.gnuplotoptions["title"] = title_token[1] continue # unknown options, raise error raise ArgumentException("Unknown gnuplot option: %s" % (option)) # sanity check if ("no-title" in self.opts.gnuplotoptions and "title" in self.opts.gnuplotoptions): raise ArgumentException("Gnuplot title AND no-title options are not allowed") def parse_local_options(self): self.ids = False parser = optparse.OptionParser() parser.usage = "throughput [options] <pcapfile> [pcapfilter]" parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-f", "--flow", dest="connections", default=None, type="string", help="specify the number of displayed ID's") parser.add_option( "-s", "--sample-length", dest="samplelength", default=1.0, type="float", help="length in seconds (float) where data is accumulated (1.0)") parser.add_option( "-p", "--per-second", dest="persecond", default=False, action="store_true", help="data is normalized as per-second average") parser.add_option( "-m", "--mode", dest="mode", default="goodput", type="string", help="layer where the data len measurement is taken (default: goodput)") parser.add_option( "-u", "--unit", dest="unit", default="byte", type="string", help="unit: bit, kilobit, megabit, gigabit, byte, kilobyte, ...") parser.add_option( "-i", "--init", dest="init", default=False, action="store_true", help="create Gnuplot template and Makefile in output-dir") parser.add_option("-t", "--stdio", dest="stdio", default=False, action="store_true", help="don't create Gnuplot data, just stdio") parser.add_option( "-o", "--output-dir", dest="outputdir", default=None, type="string", help="specify the output directory") parser.add_option("-r", "--reference-time", dest="reference_time", action="store_true", default=False, help="don't shift time axis start to " "start of flow, use global capture time start") parser.add_option( "-g", "--gnuplot-options", dest="gnuplotoptions", default=None, type="string", help="options for gnuplot, comma separated list: notitle, title=\"<newtitle>\"") self.opts, args = parser.parse_args(sys.argv[0:]) self.prepare_gnuplot_options() self.set_opts_logevel() if len(args) < 3: self.logger.critical("No pcap file argument given, exiting\n") parser.print_help(sys.stderr) sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.captcp.pcap_file_path = args[2] self.logger.info("pcap file: %s" % (self.captcp.pcap_file_path)) if self.opts.connections: self.ids = self.opts.connections.split(',') self.logger.info("show limited to the following connections: %s" % (str(self.ids))) if self.opts.samplelength != 1.0 and not self.opts.persecond: self.logger.warning("WARNING: graph is scaled to %s per %.1f seconds" % (self.opts.unit, self.opts.samplelength)) self.logger.warning("Use --per-second (-p) option if you want per-second average") def output_data(self, time, amount): if self.opts.stdio: sys.stdout.write("%5.1f %10.1f\n" % (time, amount)) else: self.throughput_file.write("%.5f %.8f\n" % (time, amount)) def pre_process_packet(self, ts, packet): if self.opts.reference_time and not self.first_packet_seen: self.first_packet_seen = ts sub_connection = self.cc.sub_connection_by_packet(packet) # only for TCP flows this can be true, therefore # no additional checks that this is TCP are required # here if not sub_connection: return # if the user applied a filter, we check it here if self.ids and not sub_connection.is_in(self.ids): return pi = TcpPacketInfo(packet) if self.opts.mode == "goodput" or self.opts.mode == "application-layer": data_len = len(packet.data.data) elif self.opts.mode == "transport-layer": data_len = len(packet.data) elif self.opts.mode == "network-layer": data_len = len(packet) elif self.opts.mode == "link-layer": data_len = len(packet) + Info.ETHERNET_HEADER_LEN else: raise NotImplementedException("mode \"%s\" not supported" % (self.opts.mode)) # time handling if not self.start_time: self.start_time = ts self.last_sample = 0.0 self.data = 0 if self.opts.reference_time: self.time_clipping_delta = Utils.ts_tofloat(self.start_time - self.first_packet_seen) timediff = Utils.ts_tofloat(ts - self.start_time) if timediff >= self.last_sample + self.opts.samplelength: # fill silent periods between a samplelength while self.last_sample + (self.opts.samplelength * 2) < timediff: self.last_sample += self.opts.samplelength self.output_data(self.last_sample + self.time_clipping_delta, 0) if self.opts.persecond: amount = U.byte_to_unit(self.data / self.opts.samplelength, self.opts.unit) else: amount = U.byte_to_unit(self.data, self.opts.unit) self.output_data(self.last_sample + self.opts.samplelength + self.time_clipping_delta, amount) self.data = 0 self.last_sample += self.opts.samplelength self.total_data_len += data_len self.data += data_len self.end_time = ts def process_final(self): # check if we processes any packet at all if not self.end_time or not self.start_time: self.logger.error("No data in tracefile found!") self.logger.error("Use tcpdump and view capture file" ". E.g. not all Ethernet types are supported") if not self.opts.stdio: self.close_data_files() return # fine, packets where captured timediff = Utils.ts_tofloat(self.end_time - self.start_time) self.logger.warning("total data (%s): %d %s (%s)" % (self.opts.mode, U.byte_to_unit(self.total_data_len, self.opts.unit), self.opts.unit, U.best_match(self.total_data_len))) self.logger.warning("throughput (%s): %.2f %s/s (%s/s)" % (self.opts.mode, U.byte_to_unit(float(self.total_data_len) / timediff, self.opts.unit), self.opts.unit, U.best_match(float(self.total_data_len) / timediff))) if not self.opts.stdio: self.close_data_files() self.logger.warning("now execute (cd %s; make preview)" % (self.opts.outputdir)) class InFlightMod(Mod): def initialize(self): self.parse_local_options() self.packet_sequence = list() self.packet_prev = False self.inflight_max = 0 self.start_time = False if not self.opts.stdio: self.check_options() if self.opts.init: self.create_gnuplot_environment() self.create_data_files() def parse_local_options(self): self.ids = False parser = optparse.OptionParser() parser.usage = "inflight [options] <pcapfile>" parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-f", "--data-flow", dest="connections", default=None, type="string", help="specify the number of relevant ID's") parser.add_option( "-m", "--mode", dest="mode", default="packets", type="string", help="display packets or bytes in flight (default packets)") parser.add_option( "-s", "--stdio", dest="stdio", default=False, action="store_true", help="don't create Gnuplot files, instead print to stdout") parser.add_option( "-i", "--init", dest="init", default=False, action="store_true", help="create Gnuplot template and Makefile in output-dir") parser.add_option( "-o", "--output-dir", dest="outputdir", default=None, type="string", help="specify the output directory") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: self.logger.error("no pcap file argument given, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.captcp.pcap_file_path = args[2] self.logger.info("pcap file: %s" % (self.captcp.pcap_file_path)) if not self.opts.connections: self.logger.error("No data flow specified (where the data flows)") sys.exit(ExitCodes.EXIT_CMD_LINE) (self.connection_id, self.data_flow_id) = self.opts.connections.split('.') if int(self.data_flow_id) == 1: self.ack_flow_id = 2 elif int(self.data_flow_id) == 2: self.ack_flow_id = 1 else: raise ArgumentException("sub flow must be 1 or 2") sys.stderr.write("# connection: %s (data flow: %s, ACK flow: %s)\n" % (self.connection_id, self.data_flow_id, self.ack_flow_id)) def create_gnuplot_environment(self): gnuplot_filename = "inflight.gpi" makefile_filename = "Makefile" if self.opts.mode == "bytes": ylabel = 'set ylabel "Bytes"' else: ylabel = 'set ylabel "Packets"' tmpl = string.Template(TemplateMod().get_content_by_name("inflight")) tmpl = tmpl.substitute(YLABEL=ylabel) filepath = "%s/%s" % (self.opts.outputdir, gnuplot_filename) fd = open(filepath, 'w') fd.write("%s" % (tmpl)) fd.close() filepath = "%s/%s" % (self.opts.outputdir, makefile_filename) fd = open(filepath, 'w') fd.write("%s" % (TemplateMod().get_content_by_name("gnuplot"))) fd.close() def check_options(self): if not self.opts.outputdir: self.logger.error("No output directory specified: --output-dir") sys.exit(ExitCodes.EXIT_CMD_LINE) if not os.path.exists(self.opts.outputdir): self.logger.error("Not a valid directory: \"%s\"" % (self.opts.outputdir)) sys.exit(ExitCodes.EXIT_CMD_LINE) def create_data_files(self): self.filepath = \ "%s/%s" % (self.opts.outputdir, "inflight.data") self.file = open(self.filepath, 'w') def close_data_files(self): self.file.close() def process_data_flow(self, ts, packet): pi = TcpPacketInfo(packet) data = (pi.seq, ts, packet) self.packet_sequence.append(data) def process_ack_flow(self, ts, packet): pi = TcpPacketInfo(packet) for i in list(self.packet_sequence): if pi.ack >= i[0]: self.packet_sequence.remove(i) def gnuplot_out(self, time, is_data): #if self.packet_prev: # self.file.write("%.5f %d\n" % (time - 0.00001, self.packet_prev)) if self.opts.mode == "bytes": sequence_size = sum(int(len(packet[2])) for packet in self.packet_sequence) self.file.write("%.5f %d\n" % (time, sequence_size)) else: self.file.write("%.5f %d\n" % (time, len(self.packet_sequence))) #self.packet_prev = len(self.packet_sequence) def stdio_out(self, time, is_data): if is_data: kind = "TX" else: kind = "RX" if self.opts.mode == "bytes": sequence_size = sum(int(len(packet[2])) for packet in self.packet_sequence) sys.stdout.write("%.5f %s %d\t%s\n" % (time, kind, sequence_size, '#' * len(self.packet_sequence))) self.inflight_max = max(self.inflight_max, sequence_size) else: sys.stdout.write("%.5f %s %d\t%s\n" % (time, kind, len(self.packet_sequence), '#' * len(self.packet_sequence))) self.inflight_max = max(self.inflight_max, len(self.packet_sequence)) def pre_process_packet(self, ts, packet): sub_connection = self.cc.sub_connection_by_packet(packet) if not sub_connection: return if sub_connection.sub_connection_id == int(self.data_flow_id): self.process_data_flow(ts, packet) is_data = True elif sub_connection.sub_connection_id == int(self.ack_flow_id): self.process_ack_flow(ts, packet) is_data = False else: raise InternalException time = Utils.ts_tofloat(ts - self.cc.capture_time_start) if self.opts.stdio: self.stdio_out(time, is_data) else: self.gnuplot_out(time, is_data) def process_final(self): if not self.opts.stdio: self.close_data_files() else: sys.stdout.write("# inflight max %d packets\n" % (self.inflight_max)) class SpacingMod(Mod): def initialize(self): self.parse_local_options() self.packet_sequence = list() self.packet_prev = False self.start_time = False self.prev_tx_time = False self.prev_rx_time = False self.tx_time_samples = list() self.rx_time_samples = list() self.capture_time_start = False if not self.opts.stdio: self.check_options() if self.opts.init: self.create_gnuplot_environment() self.create_data_files() def parse_local_options(self): self.ids = False parser = optparse.OptionParser() parser.usage = "spacing [options] <pcapfile>" parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-f", "--data-flow", dest="connections", default=None, type="string", help="specify the number of relevant ID's") parser.add_option( "-m", "--mode", dest="mode", default="packets", type="string", help="display packets or bytes in flight (default packets)") parser.add_option( "-s", "--stdio", dest="stdio", default=False, action="store_true", help="don't create Gnuplot files, instead print to stdout") parser.add_option( "-i", "--init", dest="init", default=False, action="store_true", help="create Gnuplot template and Makefile in output-dir") parser.add_option( "-o", "--output-dir", dest="outputdir", default=None, type="string", help="specify the output directory") parser.add_option( "-a", "--samples", dest="sample_no", default=10, type="int", help="number of packet sampled (default: 10)") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: self.logger.error("no pcap file argument given, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.captcp.pcap_file_path = args[2] self.logger.info("pcap file: %s" % (self.captcp.pcap_file_path)) if not self.opts.connections: self.logger.error("No data flow specified! Call \"captcp statistic\"") sys.exit(ExitCodes.EXIT_CMD_LINE) (self.connection_id, self.data_flow_id) = self.opts.connections.split('.') if int(self.data_flow_id) == 1: self.ack_flow_id = 2 elif int(self.data_flow_id) == 2: self.ack_flow_id = 1 else: raise ArgumentException("sub flow must be 1 or 2") sys.stderr.write("# connection: %s (data flow: %s, ACK flow: %s)\n" % (self.connection_id, self.data_flow_id, self.ack_flow_id)) def create_gnuplot_environment(self): gnuplot_filename = "spacing.gpi" makefile_filename = "Makefile" filepath = "%s/%s" % (self.opts.outputdir, gnuplot_filename) fd = open(filepath, 'w') fd.write("%s" % (TemplateMod().get_content_by_name("spacing"))) fd.close() filepath = "%s/%s" % (self.opts.outputdir, makefile_filename) fd = open(filepath, 'w') fd.write("%s" % (TemplateMod().get_content_by_name("gnuplot"))) fd.close() def check_options(self): if not self.opts.outputdir: self.logger.error("No output directory specified: --output-dir") sys.exit(ExitCodes.EXIT_CMD_LINE) if not os.path.exists(self.opts.outputdir): self.logger.error("Not a valid directory: \"%s\"" % (self.opts.outputdir)) sys.exit(ExitCodes.EXIT_CMD_LINE) def create_data_files(self): self.tx_filepath = "%s/%s" % (self.opts.outputdir, "tx.data") self.tx_file = open(self.tx_filepath, 'w') self.rx_filepath = "%s/%s" % (self.opts.outputdir, "rx.data") self.rx_file = open(self.rx_filepath, 'w') def close_data_files(self): self.tx_file.close() self.rx_file.close() def process_data_flow(self, ts, packet): pi = TcpPacketInfo(packet) data = (pi.seq, ts, packet) self.packet_sequence.append(data) def process_ack_flow(self, ts, packet): pi = TcpPacketInfo(packet) for i in list(self.packet_sequence): if pi.ack >= i[0]: self.packet_sequence.remove(i) def gnuplot_out(self, time, delta, is_data_flow): if is_data_flow: self.tx_file.write("%.5f %.5f\n" % (time, delta)) else: self.rx_file.write("%.5f %.5f\n" % (time, delta)) def stdio_out(self, time, delta, is_data_flow): if is_data_flow: pre = "TX" else: pre = "RX" sys.stdout.write("%s %.5f %.5f\n" % (pre, time, delta)) def pre_process_packet(self, ts, packet): sub_connection = self.cc.sub_connection_by_packet(packet) if not sub_connection: return if not self.capture_time_start: self.capture_time_start = ts time = Utils.ts_tofloat(ts - self.capture_time_start) pi = TcpPacketInfo(packet) delta = 0.0 is_data_flow = None if sub_connection.sub_connection_id == int(self.data_flow_id): if not self.prev_tx_time: self.prev_tx_time = time return delta = time - self.prev_tx_time self.prev_tx_time = time is_data_flow = True self.tx_time_samples.append(delta) if len(self.tx_time_samples) < self.opts.sample_no: return elif sub_connection.sub_connection_id == int(self.ack_flow_id): if not self.prev_rx_time: self.prev_rx_time = time return delta = time - self.prev_rx_time self.prev_rx_time = time is_data_flow = False self.rx_time_samples.append(delta) if len(self.rx_time_samples) < self.opts.sample_no: return else: raise InternalException tmp = 0.0 if is_data_flow: for i in self.tx_time_samples: tmp += i tmp /= len(self.tx_time_samples) else: for i in self.rx_time_samples: tmp += i tmp /= len(self.rx_time_samples) if self.opts.stdio: self.stdio_out(time, tmp, is_data_flow) else: self.gnuplot_out(time, tmp, is_data_flow) if is_data_flow: self.tx_time_samples = list() else: self.rx_time_samples = list() def process_final(self): if not self.opts.stdio: self.close_data_files() class ShowMod(Mod): def initialize(self): self.parse_local_options() self.color_iter = self.color.__iter__() self.packet_no = 0 def parse_local_options(self): self.ids = False parser = optparse.OptionParser() parser.usage = "show [options] <pcapfile>" parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-i", "--connection-id", dest="connections", default=None, type="string", help="specify the number of displayed ID's") parser.add_option( "-d", "--differentiate", dest="differentiate", default="connection", type="string", help="specify if \"connection\" or \"flow\" should be colored") parser.add_option( "-m", "--match", dest="match", default=None, type="string", help="if statment is true the string is color in red") parser.add_option( "-c", "--color", dest="color", default="ansi-256", type="string", help="colored output modifier: ansi-256 (default), ansi, none") parser.add_option( "-s", "--suppress", dest="suppress", default=False, action="store_true", help="don't display other packets") parser.add_option( "-n", "--number", dest="packet_number", default=False, action="store_true", help="number the packets") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: self.logger.error("no pcap file argument given, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.captcp.pcap_file_path = args[2] self.logger.info("pcap file: %s" % (self.captcp.pcap_file_path)) self.captcp.pcap_filter = None if args[3:]: self.captcp.pcap_filter = " ".join(args[3:]) self.logger.info("pcap filter: \"" + self.captcp.pcap_filter + "\"") if self.opts.connections: self.ids = self.opts.connections.split(',') self.logger.info("show limited to the following connections: %s" % (str(self.ids))) if self.opts.differentiate != "connection" and self.opts.differentiate != "flow": self.logger.error("only connection or sub-connection allowed for --d") sys.exit(ExitCodes.EXIT_CMD_LINE) if self.opts.color == "ansi-256": self.color = RainbowColor(mode=RainbowColor.ANSI256) elif self.opts.color == "ansi": self.color = RainbowColor(mode=RainbowColor.ANSI) elif self.opts.color == "none": self.color = RainbowColor(mode=RainbowColor.DISABLE) # this provides an sandbox where the variables # are made public, this a match can be coded # as "sackblocks > 2" instead of # "opts[foo].sackblocks > 2 # # return color is match is True # return False if the packet should not displayed # return None if the packet should be displayed # with no color change def match(self, ts, packet, packet_len, pi): # default is un-machted match = False # ip sip = pi.sip dip = pi.dip # tcp seq = pi.seq ack = pi.ack win = pi.win urp = pi.urp sum = pi.sum sport = pi.sport dport = pi.dport ack_flag = pi.is_ack_flag() syn_flag = pi.is_syn_flag() urg_flag = pi.is_urg_flag() psh_flag = pi.is_psh_flag() fin_flag = pi.is_fin_flag() rst_flag = pi.is_rst_flag() ece_flag = pi.is_ece_flag() cwr_flag = pi.is_cwr_flag() # tcp options mss = pi.options['mss'] wsc = pi.options['wsc'] tsval = pi.options['tsval'] tsecr = pi.options['tsecr'] sackok = pi.options['sackok'] sackblocks = pi.options['sackblocks'] exec "if " + self.opts.match + ": match = True" if match: return self.color.color_palette['red'] else: if self.opts.suppress: return False # dont change the color if nothing happends return self.color.color_palette['end'] def seq_plus(self, seq, length): return seq + length def pre_process_packet(self, ts, packet): self.packet_no += 1 sub_connection = self.cc.sub_connection_by_packet(packet) # only for TCP flows this can be true, therefore # no additional checks that this is TCP are required # here if not sub_connection: return # if the user applied a filter, we check it here if self.ids: if not sub_connection.is_in(self.ids): return # check if we already assigned a color to this # sub_connection, if not we do it now if self.opts.differentiate == "connection": if "color" not in sub_connection.connection.user_data: sub_connection.connection.user_data["color"] = \ self.color_iter.infinite_next() elif self.opts.differentiate == "flow": if "color" not in sub_connection.user_data: sub_connection.user_data["color"] = \ self.color_iter.infinite_next() pi = TcpPacketInfo(packet) data_len = len(packet.data.data) time = Utils.ts_tofloat(ts - self.cc.capture_time_start) # color init if self.opts.differentiate == "connection": line = sub_connection.connection.user_data["color"] else: line = sub_connection.user_data["color"] if self.opts.match: line = self.match(time, packet, data_len, pi) if line == False: return if self.opts.packet_number: line += "%d %.5f" % (self.packet_no, time) else: line += "%.5f" % (time) line += " %s %s:%d > %s:%d" % (pi.ipversion, pi.sip, pi.sport, pi.dip, pi.dport) line += " Flags: %s" % (pi.create_flag_brakets()) line += " seq: %u:%u ack: %u win: %u urp: %u" % ( pi.seq, self.seq_plus(pi.seq, data_len), pi.ack, pi.win, pi.urp) line += " len: %d" % (len(packet.data.data)) line += self.color["end"] line += "\n" sys.stdout.write(line) def pre_process_final(self): pass def process_packet(self, ts, packet): pass def process_final(self): pass class SoundMod(Mod): PERIOD_DATA = 1 PERIOD_ACK = 2 PERIOD_GAP = 3 FREQUENCY_DATA_START = 300 FREQUENCY_DATA_END = 1000 FREQUENCY_ACK_START = 3500 FREQUENCY_ACK_END = 5000 FREQUENCY_STEP = 50 class Sample: pass # helper class class WaveGenerator: def __init__(self, filename, samplerate=44100): self.filename = filename self.samplerate = samplerate self.signal = str() self.duration = 0.0 self.file = wave.open(filename, 'wb') def add_sample(self, frequency, duration, volume=1.0): samples = int(float(duration) * self.samplerate) period = self.samplerate / float(frequency) # in sample points omega = numpy.pi * 2 / period xaxis = numpy.arange(samples, dtype = numpy.float) ydata = 32768 * numpy.sin(xaxis * omega) signal = numpy.resize(ydata, (samples,)) self.signal += ''.join((wave.struct.pack('h', item) for item in signal)) self.duration += duration def close(self): samples = int(float(self.duration) * self.samplerate) self.file.setparams((1, 2, self.samplerate, int(float(self.samplerate) * self.duration), 'NONE', 'noncompressed')) self.file.writeframes(self.signal) self.file.close() def initialize(self): self.parse_local_options() self.capture_time_start = None self.last_packet_plus_transmission = None self.frequency_data = SoundMod.FREQUENCY_DATA_START self.frequency_ack = SoundMod.FREQUENCY_ACK_START self.packet_db = list() if not numpy: self.logger.error("Python numpy module not installed - but required for sound") sys.exit(ExitCodes.EXIT_CMD_LINE) if not wave: self.logger.error("Python wave module not installed - but required for sound") sys.exit(ExitCodes.EXIT_CMD_LINE) def parse_local_options(self): self.ids = False parser = optparse.OptionParser() parser.usage = "sound [options] <pcapfile>" parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-f", "--data-flow", dest="connections", default=None, type="string", help="specify the number of relevant ID's") parser.add_option( "-m", "--mode", dest="mode", default="packets", type="string", help="display packets or bytes in flight (default packets)") parser.add_option( "-o", "--outfile", dest="filename", default="packets.wav", type="string", help="name of the generated wav file (default: packets.wav)") parser.add_option( "-i", "--duration-min", dest="duration_min", default=None, type="float", help="minimum length of sound sample in seconds") parser.add_option( "-a", "--duration-max", dest="duration_max", default=None, type="float", help="maximum length of sound sample in seconds") parser.add_option( "-b", "--bandwidth", dest="link_bandwidth", default=100000000.0, type="int", help="netto bandwith of channel in bit/s (default 100MB)") parser.add_option( "-c", "--accelerator", dest="accelerator", default=0.0001, type="float", help="accelerator, 0.0001 times faster is default") parser.add_option( "-r", "--cut-gap-periodes", dest="cut_gap_periodes", default=0.5, type="float", help="crop longer silence periodes to this, default 0.5") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: self.logger.error("no pcap file argument given, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.captcp.pcap_file_path = args[2] self.logger.info("pcap file: %s" % (self.captcp.pcap_file_path)) if not self.opts.connections: self.logger.error("No data flow specified! Call \"captcp statistic for valid ID's\"") sys.exit(ExitCodes.EXIT_CMD_LINE) (self.connection_id, self.data_flow_id) = self.opts.connections.split('.') if int(self.data_flow_id) == 1: self.ack_flow_id = 2 elif int(self.data_flow_id) == 2: self.ack_flow_id = 1 else: raise ArgumentException("sub flow must be 1 or 2") sys.stderr.write("# connection: %s (data flow: %s, ACK flow: %s)\n" % (self.connection_id, self.data_flow_id, self.ack_flow_id)) self.wg = SoundMod.WaveGenerator(self.opts.filename) def bound(self, duration): if self.opts.duration_min: duration = max(self.opts.duration_min, duration) if self.opts.duration_max: duration = min(self.opts.duration_max, duration) return duration def data_frequency(self): self.frequency_data += SoundMod.FREQUENCY_STEP if self.frequency_data > SoundMod.FREQUENCY_DATA_END: self.frequency_data = SoundMod.FREQUENCY_DATA_START return self.frequency_data def ack_frequency(self): self.frequency_ack += SoundMod.FREQUENCY_STEP if self.frequency_ack > SoundMod.FREQUENCY_ACK_END: self.frequency_ack = SoundMod.FREQUENCY_ACK_START return self.frequency_ack def calc_duration(self, packet): packet_len = (len(packet) + Info.ETHERNET_HEADER_LEN) * 8 duration = 1.0 / (float(self.opts.link_bandwidth) / packet_len) return duration def add_silence(self, time): if not len(self.packet_db): return last_sample = self.packet_db[-1] assert(last_sample.end != time) if last_sample.end > time: self.logger.error(" time anomalie: last_sample: %lf current time: %lf" % (last_sample.end, time)) return sample = SoundMod.Sample() sample.type = SoundMod.PERIOD_GAP sample.start = last_sample.end sample.end = time self.packet_db.append(sample) def account_packet(self, time, is_data_flow, packet): # if required (mostly), add silence period self.add_silence(time) sample = SoundMod.Sample() sample.type = SoundMod.PERIOD_DATA if is_data_flow else SoundMod.PERIOD_ACK # it can happend that the time from the last packet # is larger as time. But we cannot add a real clever solution # to this problem because we don't know the exact time. Something # went wrong and we did not know where it went wrong sample.start = time sample.end = time + self.calc_duration(packet) self.packet_db.append(sample) def pre_process_packet(self, ts, packet): sub_connection = self.cc.sub_connection_by_packet(packet) if not sub_connection: return if not self.capture_time_start: self.capture_time_start = ts time = Utils.ts_tofloat(ts - self.capture_time_start) is_data_flow = None if sub_connection.sub_connection_id == int(self.data_flow_id): is_data_flow = True elif sub_connection.sub_connection_id == int(self.ack_flow_id): is_data_flow = False else: raise InternalException self.account_packet(time, is_data_flow, packet) def format_period(self, period): if period == SoundMod.PERIOD_DATA: return "DATA" if period == SoundMod.PERIOD_ACK: return " ACK" if period == SoundMod.PERIOD_GAP: return " GAP" def finish(self): times = dict() times[SoundMod.PERIOD_GAP] = [0, 0.0] times[SoundMod.PERIOD_DATA] = [0, 0.0] times[SoundMod.PERIOD_ACK] = [0, 0.0] for sample in self.packet_db: assert(sample.end >= sample.start) duration = sample.end - sample.start times[sample.type][0] += 1 times[sample.type][1] += duration self.logger.error("%s start: %lfs end: %lfs duration: %lfs" % (self.format_period(sample.type), sample.start, sample.end, duration)) normalized_duration = duration / self.opts.accelerator self.logger.error("\t\tnormalized duration: %lfs" % (normalized_duration)) if sample.type == SoundMod.PERIOD_GAP: normalized_duration = min(normalized_duration, self.opts.cut_gap_periodes) frequency = 2 if sample.type == SoundMod.PERIOD_DATA: normalized_duration = self.bound(normalized_duration) frequency = self.data_frequency() if sample.type == SoundMod.PERIOD_ACK: normalized_duration = self.bound(normalized_duration) frequency = self.ack_frequency() self.logger.error("\t\tbounded duration: %lfs" % (normalized_duration)) self.wg.add_sample(frequency, normalized_duration) sys.stderr.write("data time: %lfs, ack time: %lfs, silence time: %lfs\n" % (times[SoundMod.PERIOD_DATA][1], times[SoundMod.PERIOD_ACK][1], times[SoundMod.PERIOD_GAP][1])) def process_final(self): self.finish() self.wg.close() sys.stderr.write("# [x,sr] = wavread('%s');\n" % (self.opts.filename)) sys.stderr.write("# specgram(x,8192,sr); or\n") sys.stderr.write("# logfsgram(x,8192,sr);\n\n") sys.stderr.write("# wav -> mp3\n") sys.stderr.write("# ffmpeg -i packets.wav -vn -acodec libmp3lame packets.mp3\n") sys.stderr.write("# wav -> Ogg Vorbis\n") sys.stderr.write("# ffmpeg -i packets.wav -f ogg -acodec libvorbis -ab 192k packets.ogg\n") class StatisticMod(Mod): LABEL_DB_INDEX_DESCRIPTION = 0 LABEL_DB_INDEX_UNIT = 1 LABEL_DB_INDEX_INIT_VALUE = 2 LABEL_DB = { "packets-packets": [ "Packets", "packets", 0], "link-layer-byte": [ "Data link layer", "bytes ", 0], "network-layer-byte": [ "Data network layer", "bytes ", 0], "transport-layer-byte": [ "Data transport layer", "bytes ", 0], "application-layer-byte": [ "Data application layer", "bytes ", 0], "duration-timedelta": [ "Duration", "seconds ", 0.0], "link-layer-throughput-bitsecond": [ "Link layer throughput", "bit/s ", 0.0], "network-layer-throughput-bitsecond": [ "Network layer throughput", "bit/s ", 0.0], "transport-layer-throughput-bitsecond": [ "Transport layer throughput", "bit/s ", 0.0], "application-layer-throughput-bitsecond": [ "Application layer throughput", "bit/s ", 0.0], "rexmt-data-bytes": [ "Retransmissions", "bytes ", 0], "rexmt-data-packets": [ "Retransmissions", "packets", 0], "rexmt-bytes-percent": [ "Retransmissions per byte", "percent", 0.0], "rexmt-packets-percent": [ "Retransmissions per packet", "percent", 0.0], "pure-ack-packets": [ "ACK flag set but no payload", "packets", 0], "push-flag-set-packets": [ "PUSH flag set", "packets", 0], "ece-flag-set-packets": [ "TCP-ECE (ECN) flag set", "packets", 0], "cwr-flag-set-packets": [ "TCP-CWR (ECN) flag set", "packets", 0], "tl-ps-min": [ "TCP Payload Size (min)", "bytes", 0], "tl-ps-max": [ "TCP Payload Size (max)", "bytes", 0], "tl-ps-median": [ "TCP Payload Size (median)", "bytes", 0], "tl-ps-avg": [ "TCP Payload Size (avg)", "bytes", 0], "tl-iats-min": [ "TCP packet inter-arrival times (min)", "microseconds", 0], "tl-iats-max": [ "TCP packet inter-arrival times (max)", "microseconds", 0], "tl-iats-avg": [ "TCP packet inter-arrival times (avg)", "microseconds", 0], } def initialize(self): self.color = RainbowColor(mode=RainbowColor.ANSI) self.parse_local_options() self.capture_level = CaptureLevel.NETWORK_LAYER def parse_local_options(self): parser = optparse.OptionParser() parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (debug, info, warning, error)") parser.add_option( "-i", "--filter", dest="filter", default=None, type="string", help="limit number of displayed connections " + \ "\"sip:sport-dip:dport\", default \"*:*-*:*\"") parser.add_option( "-m", "--format", dest="format", default=None, type="string", help="skip summary and display only selected values") parser.add_option("-e", "--extended", dest="extended", action="store_true", help="show extended statistics about packet sizes etc.") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if self.opts.filter: self.opts.filter = self.opts.filter.split(",") else: self.opts.filter = list() if len(args) < 3: self.logger.error("no pcap file argument given, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) if self.opts.extended and not numpy: self.logger.error("Python numpy module not installed - but required for extended statistic") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.captcp.pcap_file_path = args[2] self.logger.info("pcapfile: \"%s\"" % self.captcp.pcap_file_path) def check_new_subconnection(self, sc): if len(sc.user_data): return # initialize the data values in a loop, e.g # sc.user_data["link-layer-byte"] = 0 # [...] index = StatisticMod.LABEL_DB_INDEX_INIT_VALUE for key in self.LABEL_DB: sc.user_data[key] = self.LABEL_DB[key][index] # helper variables comes here, helper # variables are marked with a leading # underscore. sc.user_data["_highest_data_seen"] = None sc.user_data["_tl_pkt_sizes"] = list() sc.user_data["_tl_iats"] = list() sc.user_data["_tl_ia_last"] = None sc.user_data["_flow_time_start"] = None sc.user_data["_flow_time_end"] = None def type_to_label(self, label): return self.LABEL_DB[label][StatisticMod.LABEL_DB_INDEX_DESCRIPTION] def right(self, text, width): return text[:width].rjust(width) def center(self, text, width): return text[:width].center(width) def left(self, text, width): return text[:width].ljust(width) def calc_max_label_length(self): max_label_length = 0 index = StatisticMod.LABEL_DB_INDEX_DESCRIPTION for i in self.LABEL_DB: max_label_length = max(max_label_length, len(str(self.LABEL_DB[i][index]))) return max_label_length + 3 def calc_max_data_length(self, statistic): max_data_length = 0 index = StatisticMod.LABEL_DB_INDEX_UNIT for i in self.LABEL_DB: max_data_length = max(max_data_length, len(str(statistic.user_data[i])) + len(self.LABEL_DB[i][index])) return max_data_length + 1 def account_general_data(self, packet): if type(packet) == dpkt.ip.IP: self.cc.statistic.packets_nl_ipv4 += 1 elif type(packet) == dpkt.ip6.IP6: self.cc.statistic.packets_nl_ipv6 += 1 elif type(packet) == dpkt.arp.ARP: self.cc.statistic.packets_nl_arp += 1 raise PacketNotSupportedException() else: self.cc.statistic.packets_nl_unknown += 1 raise PacketNotSupportedException() if type(packet.data) == dpkt.tcp.TCP: self.cc.statistic.packets_tl_tcp += 1 elif type(packet.data) == dpkt.udp.UDP: self.cc.statistic.packets_tl_udp += 1 raise PacketNotSupportedException() elif type(packet.data) == dpkt.icmp.ICMP: self.cc.statistic.packets_tl_icmp += 1 raise PacketNotSupportedException() elif type(packet.data) == dpkt.icmp6.ICMP6: self.cc.statistic.packets_tl_icmp6 += 1 raise PacketNotSupportedException() else: self.cc.statistic.packets_tl_unknown += 1 raise PacketNotSupportedException() def account_general_tcp_data(self, sc, ts, packet): sc.user_data["packets-packets"] += 1 sc.user_data["link-layer-byte"] += len(packet) + Info.ETHERNET_HEADER_LEN sc.user_data["network-layer-byte"] += int(len(packet)) sc.user_data["transport-layer-byte"] += int(len(packet.data)) sc.user_data["application-layer-byte"] += int(len(packet.data.data)) # capture start and end on a per flow basis # This will be used for flow duration and flow throughput if not sc.user_data["_flow_time_start"]: sc.user_data["_flow_time_start"] = ts sc.user_data["_flow_time_end"] = ts self.cc.statistic.packets_processed += 1 def rexmt_final(self, sc): # called at the end of traxing to check values # or do some final calculations, based on intermediate # values res = U.percent(sc.user_data["rexmt-data-bytes"], sc.user_data["application-layer-byte"]) sc.user_data["rexmt-bytes-percent"] = "%.2f" % (res) res = U.percent(sc.user_data["rexmt-data-packets"], sc.user_data["packets-packets"]) sc.user_data["rexmt-packets-percent"] = "%.2f" % (res) if len(sc.user_data["_tl_pkt_sizes"]) > 0: sc.user_data["tl-ps-min"] = "%d" % min(sc.user_data["_tl_pkt_sizes"]) sc.user_data["tl-ps-max"] = "%d" % max(sc.user_data["_tl_pkt_sizes"]) sc.user_data["tl-ps-avg"] = "%.2f" % numpy.mean(sc.user_data["_tl_pkt_sizes"]) sc.user_data["tl-ps-median"] = "%.2f" % numpy.median(sc.user_data["_tl_pkt_sizes"]) if len(sc.user_data["_tl_iats"]) > 0: sc.user_data["tl-iats-min"] = "%d" % min(sc.user_data["_tl_iats"]) sc.user_data["tl-iats-max"] = "%d" % max(sc.user_data["_tl_iats"]) sc.user_data["tl-iats-avg"] = "%d" % numpy.mean(sc.user_data["_tl_iats"]) if sc.user_data["_flow_time_start"] != sc.user_data["_flow_time_end"]: sc.user_data["duration-timedelta"] = sc.user_data["_flow_time_end"] - sc.user_data["_flow_time_start"] sc.user_data["duration-timedelta"] = Utils.ts_tofloat(sc.user_data["duration-timedelta"]) if sc.user_data["duration-timedelta"] > 0.0: sc.user_data["link-layer-throughput-bitsecond"] = "%.2f" % ((sc.user_data["link-layer-byte"] * 8) / sc.user_data["duration-timedelta"]) sc.user_data["network-layer-throughput-bitsecond"] = "%.2f" % ((sc.user_data["network-layer-byte"] * 8) / sc.user_data["duration-timedelta"]) sc.user_data["transport-layer-throughput-bitsecond"] = "%.2f" % ((sc.user_data["transport-layer-byte"] * 8) / sc.user_data["duration-timedelta"]) sc.user_data["application-layer-throughput-bitsecond"] = "%.2f" % ((sc.user_data["application-layer-byte"] * 8) / sc.user_data["duration-timedelta"]) # must be last operation! # we convert duration-timedelta to string with floating point # precision of .2 sc.user_data["duration-timedelta"] = "%.2f" % sc.user_data["duration-timedelta"] def account_rexmt(self, sc, packet, pi, ts): data_len = int(len(packet.data.data)) transport_len = int(len(packet.data)) actual_data = pi.seq + data_len if not sc.user_data["_highest_data_seen"]: # no rexmt possible, skip rexmt processing sc.user_data["_highest_data_seen"] = actual_data return if actual_data > sc.user_data["_highest_data_seen"]: # packet sequence number is highest sequence # number seen so far, no rexmt therefore sc.user_data["_highest_data_seen"] = actual_data if data_len > 0: sc.user_data["_tl_pkt_sizes"].append(transport_len) if sc.user_data["_tl_ia_last"] is not None: delta = ts - sc.user_data["_tl_ia_last"] sc.user_data["_tl_iats"].append((delta.seconds*1000000 + delta.microseconds)/1000) sc.user_data["_tl_ia_last"] = ts return if data_len == 0: # no data packet, cannot be a retransmission return # ok, rexmt happened sc.user_data["rexmt-data-packets"] += 1 # now account rexmt bytes, we add one to take care sc.user_data["rexmt-data-bytes"] += data_len def account_pure_ack(self, sc, packet, pi): if pi.is_ack_flag() and int(len(packet.data.data)) == 0: sc.user_data["pure-ack-packets"] += 1 def account_evil_bits(self, sc, packet, pi): if pi.is_psh_flag(): sc.user_data["push-flag-set-packets"] += 1 if pi.is_ece_flag(): sc.user_data["ece-flag-set-packets"] += 1 if pi.is_cwr_flag(): sc.user_data["cwr-flag-set-packets"] += 1 def account_tcp_data(self, sc, ts, packet, pi): self.account_rexmt(sc, packet, pi, ts) self.account_evil_bits(sc, packet, pi) self.account_pure_ack(sc, packet, pi) def pre_process_packet(self, ts, packet): try: self.account_general_data(packet) except PacketNotSupportedException: return sc = self.cc.sub_connection_by_packet(packet) if not sc: return InternalException() # make sure the data structure is initialized self.check_new_subconnection(sc) self.account_general_tcp_data(sc, ts, packet) # .oO guaranteed TCP packet now pi = TcpPacketInfo(packet) self.account_tcp_data(sc, ts, packet, pi) def print_one_column_sc_statistic(self, cid, sc): raise NotImplementedException("one flow connection not supported yet") def print_format_two_column(self, cid, statistic): sc1 = statistic[0] sc2 = statistic[1] left_width = self.calc_max_label_length() right_width = max(self.calc_max_data_length(sc1), self.calc_max_data_length(sc2)) # flow specific header per column # l1 = self.left("%d.1" % (cid), left_width) # r1 = self.right("%d.2" % (cid), right_width) # line_length = left_width + right_width + 1 # sys.stdout.write("\t%s %s\n" % (self.left("%s" % (l1), line_length), self.left("%s" % (r1), line_length))) ordere_list = [ "packets-packets", "duration-timedelta", "link-layer-byte", "link-layer-throughput-bitsecond", "application-layer-byte", "application-layer-throughput-bitsecond", "rexmt-data-packets", "rexmt-packets-percent", "pure-ack-packets", ] if self.opts.extended: ordere_list = [ "packets-packets", "duration-timedelta", "link-layer-byte", "network-layer-byte", "transport-layer-byte", "application-layer-byte", "link-layer-throughput-bitsecond", "network-layer-throughput-bitsecond", "transport-layer-throughput-bitsecond", "application-layer-throughput-bitsecond", "rexmt-data-bytes", "rexmt-data-packets", "rexmt-bytes-percent", "rexmt-packets-percent", "pure-ack-packets", "push-flag-set-packets", "ece-flag-set-packets", "cwr-flag-set-packets", "tl-ps-min", "tl-ps-max", "tl-ps-avg", "tl-ps-median", "tl-iats-min", "tl-iats-max", "tl-iats-avg", ] for i in ordere_list: l1 = self.left(self.type_to_label(i) + ":", left_width) r1 = self.right(str(sc1.user_data[i]) + " " + self.LABEL_DB[i][1], right_width) l2 = self.left(self.type_to_label(i) + ":", left_width) r2 = self.right(str(sc2.user_data[i])+ " " + self.LABEL_DB[i][1], right_width) line_length = left_width + right_width + 1 sys.stdout.write(" %s %s\n" % (self.left("%s %s" % (l1, r1), line_length), self.left("%s %s" % (l2, r2), line_length))) def format_human(self): one_percent = float(self.cc.statistic.packets_processed) / 100 prct_nl_arp = float(self.cc.statistic.packets_nl_arp) / one_percent prct_nl_ip = float(self.cc.statistic.packets_nl_ipv4) / one_percent prct_nl_ipv6 = float(self.cc.statistic.packets_nl_ipv6) / one_percent prct_nl_unknown = float(self.cc.statistic.packets_nl_unknown) / one_percent prct_tl_tcp = float(self.cc.statistic.packets_tl_tcp) / one_percent prct_tl_udp = float(self.cc.statistic.packets_tl_udp) / one_percent prct_tl_icmp = float(self.cc.statistic.packets_tl_icmp) / one_percent prct_tl_icmp6 = float(self.cc.statistic.packets_tl_icmp6) / one_percent prct_tl_unknown = float(self.cc.statistic.packets_tl_unknown) / one_percent sys.stdout.write("General:\n") sys.stdout.write("\tPackets processed: %5d (%7.2f%%)\n" % (self.cc.statistic.packets_processed, float(100))) sys.stdout.write("\tNetwork Layer\n") sys.stdout.write("\t ARP: %8d (%7.2f%%)\n" % (self.cc.statistic.packets_nl_arp, prct_nl_arp)) sys.stdout.write("\t IPv4: %8d (%7.2f%%)\n" % (self.cc.statistic.packets_nl_ipv4, prct_nl_ip)) sys.stdout.write("\t IPv6: %8d (%7.2f%%)\n" % (self.cc.statistic.packets_nl_ipv6, prct_nl_ipv6)) sys.stdout.write("\t Unknown: %8d (%7.2f%%)\n" % (self.cc.statistic.packets_nl_unknown, prct_nl_unknown)) sys.stdout.write("\tTransport Layer\n") sys.stdout.write("\t TCP: %8d (%7.2f%%)\n" % (self.cc.statistic.packets_tl_tcp, prct_tl_tcp)) sys.stdout.write("\t UDP: %8d (%7.2f%%)\n" % (self.cc.statistic.packets_tl_udp, prct_tl_udp)) sys.stdout.write("\t ICMP: %8d (%7.2f%%)\n" % (self.cc.statistic.packets_tl_icmp, prct_tl_icmp)) sys.stdout.write("\t ICMPv6: %8d (%7.2f%%)\n" % (self.cc.statistic.packets_tl_icmp6, prct_tl_icmp6)) sys.stdout.write("\t Unknown: %8d (%7.2f%%)\n" % (self.cc.statistic.packets_tl_unknown, prct_tl_unknown)) sys.stdout.write("\nConnections:\n") # first we sort in an separate dict d = dict() for key in self.cc.container.keys(): connection = self.cc.container[key] d[connection.connection_id] = connection for key in sorted(d.keys()): connection = d[key] sys.stdout.write("\n") sys.stdout.write("%s %d %s %s%s\n\n" % (self.color["red"], connection.connection_id, self.color["yellow"], connection, self.color["end"])) # statistic sys.stdout.write(" Packets processed: %d (%.1f%%)\n" % (connection.statistic.packets_processed, float(connection.statistic.packets_processed) / float(self.cc.statistic.packets_processed) * 100.0)) sys.stdout.write(" Duration: %.2f seconds\n" % (Utils.ts_tofloat(connection.capture_time_end - connection.capture_time_start))) sys.stdout.write("\n") if connection.sc1 and connection.sc2: sys.stdout.write("%s" % (self.color["yellow"])) sys.stdout.write(" Flow %s.1 %s" % (connection.connection_id, connection.sc1)) sys.stdout.write("%s\n" % (self.color["end"])) sys.stdout.write("%s" % (self.color["green"])) sys.stdout.write(" Flow %s.2 %s" % (connection.connection_id, connection.sc2)) sys.stdout.write("%s\n" % (self.color["end"])) self.print_format_two_column(connection.connection_id, [connection.sc1, connection.sc2]) elif connection.sc1: sys.stdout.write(" Flow %s.1 %s\n" % (connection.connection_id, connection.sc1)) self.print_one_column_sc_statistic(connection.connection_id, connection.sc1) else: raise InternalException("sc1 should be the only one here") sys.stdout.write("\n") def not_limited(self, connection): res = list() if len(self.opts.filter) <= 0: return True for filter in self.opts.filter: try: (stupple, dtupple) = filter.split("-") (sip, sport) = stupple.split(":") (dip, dport) = dtupple.split(":") except ValueError: self.logger.error("not a valid filter string: \"%s\"" % (filter)) sys.exit(ExitCodes.EXIT_CMD_LINE) if sip != "*" and sip != connection.sip: res.append(True) continue if dip != "*" and dip != connection.dip: res.append(True) continue if sport != "*" and sport != connection.sport: res.append(True) continue if dport != "*" and dport != connection.dport: res.append(True) continue res.append(False) if False in res: return True return False def format_machine(self): for key in self.cc.container.keys(): connection = self.cc.container[key] if connection.sc1: if self.not_limited(connection.sc1): sys.stdout.write(self.opts.format % (connection.sc1.user_data)) sys.stdout.write("\n") if connection.sc2: if self.not_limited(connection.sc2): sys.stdout.write(self.opts.format % (connection.sc2.user_data)) sys.stdout.write("\n") def process_final_data(self): # first we sort in an separate dict d = dict() for key in self.cc.container.keys(): connection = self.cc.container[key] d[connection.connection_id] = connection for key in sorted(d.keys()): connection = d[key] if connection.sc1: self.rexmt_final(connection.sc1) if connection.sc2: self.rexmt_final(connection.sc2) def process_final(self): self.process_final_data() self.format_machine() if self.opts.format else self.format_human() class ConnectionAnimationMod(Mod): LOCAL = 1 REMOTE = 2 def initialize(self): self.parse_local_options() self.write_js_header() self.reference_time = None self.first_packt_in_flow = None self.rtt_data = dict() self.acceleration = 0.001 def write_js_header(self): js_filepath = "%s/%s" % (self.opts.outputdir, "data.js") self.js_fd = open(js_filepath, 'w') self.js_fd.write("var image_path = \"images/old-computer.png\"\n") self.js_fd.write("function main() {\n\n") def create_html_environment(self): path = "%s/data/connection-animation-data/" % \ (os.path.dirname(os.path.realpath(__file__))) distutils.dir_util.copy_tree(path, self.opts.outputdir) def check_options(self): if not self.opts.outputdir: self.logger.error("No output directory specified: --output-dir") sys.exit(ExitCodes.EXIT_CMD_LINE) if not os.path.exists(self.opts.outputdir): self.logger.error("Not a valid directory: \"%s\"" % (self.opts.outputdir)) sys.exit(ExitCodes.EXIT_CMD_LINE) def parse_local_options(self): self.ids = False parser = optparse.OptionParser() parser.usage = "animation [options] <pcapfile>" parser.add_option( "-v", "--verbose", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-f", "--data-flow", dest="connections", default=None, type="string", help="specify the number of relevant ID's") parser.add_option( "-o", "--output-dir", dest="outputdir", default=None, type="string", help="specify the output directory") parser.add_option( "-i", "--init", dest="init", default=False, action="store_true", help="create/overwrite HTML/Javascript files in output-dir") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() if len(args) < 3: self.logger.error("no pcap file argument given, exiting") sys.exit(ExitCodes.EXIT_CMD_LINE) self.captcp.print_welcome() self.captcp.pcap_file_path = args[2] self.logger.info("pcap file: %s" % (self.captcp.pcap_file_path)) self.check_options() if self.opts.init: self.create_html_environment() if not self.opts.connections: self.logger.error("No data flow specified! Call \"captcp statistic for valid ID's\"") sys.exit(ExitCodes.EXIT_CMD_LINE) try: (self.connection_id, self.local_flow_id) = self.opts.connections.split('.') except ValueError: self.logger.error("You specified a connection not a flow") sys.exit(ExitCodes.EXIT_CMD_LINE) if int(self.local_flow_id) == 1: self.remote_flow_id = 2 elif int(self.local_flow_id) == 2: self.remote_flow_id = 1 else: raise ArgumentException("sub flow must be 1 or 2") self.logger.error("connection: %s (local flow: %s, remote flow: %s)" % (self.connection_id, self.local_flow_id, self.remote_flow_id)) def calc_rtt(self, ts, packet, tpi): if not tpi.is_syn_flag(): return if not tpi.is_ack_flag(): self.logger.debug("TWH - SYN received") self.rtt_data["syn-received"] = dict() self.rtt_data["syn-received"]["ts"] = ts self.rtt_data["syn-received"]["seq"] = tpi.seq elif tpi.is_ack_flag() and tpi.ack == self.rtt_data["syn-received"]["seq"] + 1: self.rtt_data["twh-rtt"] = Utils.ts_tofloat(ts - \ self.rtt_data["syn-received"]["ts"]) * 1000.0 self.logger.debug("TWH - SYN/ACK received (%.3f ms later)" % (self.rtt_data["twh-rtt"])) self.rtt_data["twh-delay"] = self.rtt_data["twh-rtt"] / 2.0 self.logger.debug("set delay to %.3f ms " % (self.rtt_data["twh-delay"])) # we add a small margin buffer of 20% self.rtt_data["twh-delay"] *= 0.9 self.logger.debug("add 20%% buffer %.3f ms " % (self.rtt_data["twh-delay"])) def process_local_side(self, ts, packet, tpi): """ we animate the packet send from us to the peer """ if not self.rtt_data.has_key("twh-delay"): self.logger.error("The chosen flow does not contain SYN packets") sys.exit(ExitCodes.EXIT_CMD_LINE) td = Utils.ts_tofloat(ts - self.reference_time) * 1000.0 / self.acceleration self.js_fd.write("\t//ts: %s\n" % (td)) self.js_fd.write("\tregister_packet(%d, r1, r2, 'data', %d, %d);\n" % (td, len(packet) / 2, self.rtt_data["twh-delay"] / self.acceleration)) def process_remote_side(self, ts, packet, tpi): """ we animate the packet send from remote to us """ if not self.rtt_data.has_key("twh-delay"): self.logger.error("The chosen flow does not contain SYN packets") sys.exit(ExitCodes.EXIT_CMD_LINE) td = Utils.ts_tofloat(ts - self.reference_time) * 1000.0 / self.acceleration self.js_fd.write("\t//ts: %s\n" % (td)) self.js_fd.write("\tregister_packet(%d, r2, r1, 'data', %d, %d);\n" % ((td - (self.rtt_data["twh-delay"] / \ self.acceleration)), len(packet) / 2, \ self.rtt_data["twh-delay"] / self.acceleration)) def pre_process_packet(self, ts, packet): if not PacketInfo.is_tcp(packet): return if not self.cc.is_packet_connection(packet, int(self.connection_id)): return False if not self.reference_time: self.reference_time = ts sub_connection = self.cc.sub_connection_by_packet(packet) if not self.first_packt_in_flow: self.first_packt_in_flow = sub_connection.sub_connection_id tpi = TcpPacketInfo(packet, module=self) self.calc_rtt(ts, packet, tpi) def pre_process_final(self): return # FIXME if self.first_packt_in_flow == int(self.remote_flow_id): self.reference_time += self.rtt_data["twh-delay"] def process_packet(self, ts, packet): if not PacketInfo.is_tcp(packet): return if not self.cc.is_packet_connection(packet, int(self.connection_id)): return False if not self.reference_time: self.reference_time = ts tpi = TcpPacketInfo(packet) sub_connection = self.cc.sub_connection_by_packet(packet) if sub_connection.sub_connection_id == int(self.local_flow_id): self.process_local_side(ts, packet, tpi) elif sub_connection.sub_connection_id == int(self.remote_flow_id): self.process_remote_side(ts, packet, tpi) else: raise InternalException def process_final(self): self.js_fd.write("}\n") self.js_fd.close() self.logger.error("finish, now (cd %s; google-chrome index.html)" % (self.opts.outputdir)) class SocketStatisticsMod(Mod): class TimelineEvent: pass class SsConnection: def __init__(self): self.timeline = list() def start(self): self.start = datetime.datetime.now() def snapshot(self): now = datetime.datetime.now() timeline_event = SocketStatisticsMod.TimelineEvent() self.timeline.append((now, timeline_event)) return timeline_event def timedelta_to_milli(self, td): return td.microseconds / 1000.0 + (td.seconds + td.days * 86400) * 1000 def create_data_dir(self, name): path = os.path.join(self.opts.outputdir, name) if os.path.exists(path): if not self.opts.force: self.logger.error("Directory already exists: %s - " "remove or force to overwrite, ignore now" % (path)) return None self.logger.info("clean directory %s now" % (path)) shutil.rmtree(path) self.logger.info("create directory for connection %s" % (path)) os.makedirs(path) return path def create_gnuplot_env_rtt(self, path): gnuplot_filename = "ss-rtt.gpi" makefile_filename = "Makefile" xrange_str = "" yrange_str = "" if (self.timeframe_start != None) and (self.timeframe_end != None): xrange_str = "set xrange [%s:%s]" % \ (self.timeframe_start, self.timeframe_end) title='set title "TCP RTT and RTO"' if "no-title" in self.opts.gnuplotoptions: title = 'set notitle' if "title" in self.opts.gnuplotoptions: title = "set title \"%s\"" % (self.opts.gnuplotoptions["title"]) logscaley="" if "y-logscale" in self.opts.gnuplotoptions: logscaley = "set logscale y" logscalex="" if "x-logscale" in self.opts.gnuplotoptions: logscalex = "set logscale x" size_ratio="" if "size-ratio" in self.opts.gnuplotoptions: size_ratio = "set size ratio %.2f" % (self.opts.gnuplotoptions["size-ratio"]) # Normal format or extended format tmpl = string.Template(TemplateMod().get_content_by_name("ss-rtt")) gpi_cmd = tmpl.substitute(XRANGE=xrange_str, TITLE=title, SIZE_RATIO=size_ratio, YRANGE="") filepath = "%s/%s" % (path, gnuplot_filename) fd = open(filepath, 'w') fd.write("%s" % (gpi_cmd)) fd.close() filepath = "%s/%s" % (path, makefile_filename) fd = open(filepath, 'w') fd.write("%s" % (TemplateMod().get_content_by_name("gnuplot"))) fd.close() def check_rtt_env(self, full_path): if os.path.exists(full_path): return os.makedirs(full_path) self.create_gnuplot_env_rtt(full_path) def write_rtt(self, path, time_delta, rtt, rtt_var, rto): full_path = os.path.join(path, "rtt") self.check_rtt_env(full_path) rtt_data_path = os.path.join(full_path, "rtt.data") rtt_var_data_path = os.path.join(full_path, "rtt_var.data") rto_data_path = os.path.join(full_path, "rto.data") with open(rtt_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, rtt)) with open(rtt_var_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, rtt_var)) with open(rto_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, rto)) def create_gnuplot_env_cwnd_ssthresh(self, path): gnuplot_filename = "ss-cwnd-ssthresh.gpi" makefile_filename = "Makefile" xrange_str = "" yrange_str = "" if (self.timeframe_start != None) and (self.timeframe_end != None): xrange_str = "set xrange [%s:%s]" % \ (self.timeframe_start, self.timeframe_end) title='set title "TCP Congestion Window and Slow Start Threshold"' if "no-title" in self.opts.gnuplotoptions: title = 'set notitle' if "title" in self.opts.gnuplotoptions: title = "set title \"%s\"" % (self.opts.gnuplotoptions["title"]) logscaley="" if "y-logscale" in self.opts.gnuplotoptions: logscaley = "set logscale y" logscalex="" if "x-logscale" in self.opts.gnuplotoptions: logscalex = "set logscale x" size_ratio="" if "size-ratio" in self.opts.gnuplotoptions: size_ratio = "set size ratio %.2f" % (self.opts.gnuplotoptions["size-ratio"]) # Normal format or extended format tmpl = string.Template(TemplateMod().get_content_by_name("ss-cwnd-ssthresh")) gpi_cmd = tmpl.substitute(XRANGE=xrange_str, TITLE=title, SIZE_RATIO=size_ratio, YRANGE="") filepath = "%s/%s" % (path, gnuplot_filename) fd = open(filepath, 'w') fd.write("%s" % (gpi_cmd)) fd.close() filepath = "%s/%s" % (path, makefile_filename) fd = open(filepath, 'w') fd.write("%s" % (TemplateMod().get_content_by_name("gnuplot"))) fd.close() def check_cwnd_ssthresh_env(self, full_path): if os.path.exists(full_path): return os.makedirs(full_path) self.create_gnuplot_env_cwnd_ssthresh(full_path) def write_cwnd_ssthresh(self, path, time_delta, cwnd, ssthresh): full_path = os.path.join(path, "cwnd-ssthresh") self.check_cwnd_ssthresh_env(full_path) cwnd_data_path = os.path.join(full_path, "cwnd.data") ssthresh_data_path = os.path.join(full_path, "ssthresh.data") if cwnd: with open(cwnd_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, cwnd)) if ssthresh: with open(ssthresh_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, ssthresh)) def create_gnuplot_env_skmem(self, path): gnuplot_filename = "ss-skmem.gpi" makefile_filename = "Makefile" xrange_str = "" yrange_str = "" if (self.timeframe_start != None) and (self.timeframe_end != None): xrange_str = "set xrange [%s:%s]" % \ (self.timeframe_start, self.timeframe_end) title='set title "Socket Memory"' if "no-title" in self.opts.gnuplotoptions: title = 'set notitle' if "title" in self.opts.gnuplotoptions: title = "set title \"%s\"" % (self.opts.gnuplotoptions["title"]) logscaley="" if "y-logscale" in self.opts.gnuplotoptions: logscaley = "set logscale y" logscalex="" if "x-logscale" in self.opts.gnuplotoptions: logscalex = "set logscale x" size_ratio="" if "size-ratio" in self.opts.gnuplotoptions: size_ratio = "set size ratio %.2f" % (self.opts.gnuplotoptions["size-ratio"]) # Normal format or extended format tmpl = string.Template(TemplateMod().get_content_by_name("ss-skmem")) gpi_cmd = tmpl.substitute(XRANGE=xrange_str, TITLE=title, SIZE_RATIO=size_ratio, YRANGE="") filepath = "%s/%s" % (path, gnuplot_filename) fd = open(filepath, 'w') fd.write("%s" % (gpi_cmd)) fd.close() filepath = "%s/%s" % (path, makefile_filename) fd = open(filepath, 'w') fd.write("%s" % (TemplateMod().get_content_by_name("gnuplot"))) fd.close() def check_skmem_env(self, full_path): if os.path.exists(full_path): return os.makedirs(full_path) self.create_gnuplot_env_skmem(full_path) def write_skmem(self, path, time_delta, skmem): full_path = os.path.join(path, "skmem") self.check_skmem_env(full_path) backlog_data_path = os.path.join(full_path, "backlog.data") rmem_alloc_data_path = os.path.join(full_path, "rmem-alloc.data") rcvbuf_data_path = os.path.join(full_path, "rcvbuf.data") wmem_alloc_data_path = os.path.join(full_path, "wmem-alloc.data") sndbuf_data_path = os.path.join(full_path, "sndbuf.data") fwd_alloc_data_path = os.path.join(full_path, "fwd-alloc.data") wmem_queued_data_path = os.path.join(full_path, "wmem-queued.data") optmem_data_path = os.path.join(full_path, "optmem.data") with open(backlog_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, skmem["SK_MEMINFO_BACKLOG"])) with open(rmem_alloc_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, skmem["SK_MEMINFO_RMEM_ALLOC"])) with open(rcvbuf_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, skmem["SK_MEMINFO_RCVBUF"])) with open(wmem_alloc_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, skmem["SK_MEMINFO_WMEM_ALLOC"])) with open(sndbuf_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, skmem["SK_MEMINFO_SNDBUF"])) with open(fwd_alloc_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, skmem["SK_MEMINFO_FWD_ALLOC"])) with open(wmem_queued_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, skmem["SK_MEMINFO_WMEM_QUEUED"])) with open(optmem_data_path, "a") as fd: fd.write("%s %s\n" %(time_delta, skmem["SK_MEMINFO_OPTMEM"])) def write_data_files(self, path, time_delta, data): # convert msec time delta to seconds time_delta_sec = "%.3f" % (float(time_delta) / 1000.0) if "rtt" in data and "rto" in data: self.write_rtt(path, time_delta_sec, data["rtt"]["rtt"], data["rtt"]["rttvar"], data["rto"]) if "cwnd" in data or "ssthresh" in data: cwnd = None if not "cwnd" in data else data["cwnd"] ssthresh = None if not "ssthresh" in data else data["ssthresh"] self.write_cwnd_ssthresh(path, time_delta_sec, cwnd, ssthresh) if "skmem" in data: self.write_skmem(path, time_delta_sec, data["skmem"]) def write_db(self): for key, value in self.db.items(): path = self.create_data_dir(key) if path == None: continue for snapshot in value.timeline: time = snapshot[0] event = snapshot[1] time_delta = self.timedelta_to_milli(time - value.start) self.write_data_files(path, time_delta, event.data) def execute_ss(self): cmd = ["ss", "-i", "-m", "-e", "-t", "-n"] p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=None) out, err = p.communicate() return out def connection_handle(self, local_addr, peer_addr): key = "%s-%s" % (local_addr, peer_addr) if key not in self.db: self.db[key] = SocketStatisticsMod.SsConnection() self.db[key].start() return self.db[key] def parse_std_line(self, std): retdata = dict() timer_info = uid = None std = std.split() if len(std) not in (7, 8, 9, 10): sys.stderr.write("Corrupt line format: %d (%s)\n" % (len(std), std)) return None if len(std) == 7: # 'ESTAB', '10136', '0', '198.18.20.107:5001', '198.18.10.144:53291', # 'ino:0', 'sk:ffff88003c8e8700'] retdata["state"] = std[0] retdata["recv_q"] = std[1] retdata["send_q"] = std[2] retdata["local_addr"] = std[3] retdata["peer_addr"] = std[4] retdata["ino"] = std[5] retdata["sk"] = std[6] if len(std) == 8: # ['ESTAB', '0', '0', '198.18.20.107:5001', '198.18.10.144:53282', # 'uid:1000', 'ino:180214', 'sk:ffff880029e00700'] retdata["state"] = std[0] retdata["recv_q"] = std[1] retdata["send_q"] = std[2] retdata["local_addr"] = std[3] retdata["peer_addr"] = std[4] retdata["uid"] = std[5] retdata["ino"] = std[6] retdata["sk"] = std[7] if len(std) == 9 or len(std) == 10: # ['ESTAB', '0', '0', '10.1.11.169:55427', '192.168.1.64:80', # 'timer:(keepalive,15sec,0)', 'uid:1000', 'ino:185811', 'sk:ffff88001894a300'] retdata["state"] = std[0] retdata["recv_q"] = std[1] retdata["send_q"] = std[2] retdata["local_addr"] = std[3] retdata["peer_addr"] = std[4] retdata["timer"] = std[5] retdata["uid"] = std[6] retdata["ino"] = std[7] retdata["sk"] = std[8] return retdata def parse_skmem(self, data): d = dict() for entry in ["SK_MEMINFO_RCVBUF", "SK_MEMINFO_SNDBUF", "SK_MEMINFO_BACKLOG", "SK_MEMINFO_RMEM_ALLOC", "SK_MEMINFO_WMEM_ALLOC", "SK_MEMINFO_FWD_ALLOC", "SK_MEMINFO_WMEM_QUEUED", "SK_MEMINFO_OPTMEM"]: d[entry] = 0 for date in data: # we start with two letter prefix if date.startswith("rb"): d["SK_MEMINFO_RCVBUF"] = date[2:] continue if date.startswith("tb"): d["SK_MEMINFO_SNDBUF"] = date[2:] continue if date.startswith("bl"): d["SK_MEMINFO_BACKLOG"] = date[2:] continue # one leter prefix if date.startswith("r"): d["SK_MEMINFO_RMEM_ALLOC"] = date[1:] continue if date.startswith("t"): d["SK_MEMINFO_WMEM_ALLOC"] = date[1:] continue if date.startswith("f"): d["SK_MEMINFO_FWD_ALLOC"] = date[1:] continue if date.startswith("w"): d["SK_MEMINFO_WMEM_QUEUED"] = date[1:] continue if date.startswith("o"): d["SK_MEMINFO_OPTMEM"] = date[1:] continue self.logger.warning("Unknown socket buffer value: %s" % (date)) if int(d["SK_MEMINFO_FWD_ALLOC"]) >= 4294966736: self.logger.warning("SK_MEMINFO_FWD_ALLOC >= 4294966736 - reduce to 0") d["SK_MEMINFO_FWD_ALLOC"] = 0 return d def parse_ext_line(self, ext): d = dict() # skmem:(r25216,rb172144,t0,tb23400,f3456,w0,o0,bl0) ts sack cubic wscale:7,7 # rto:1404 rtt:468/234 ato:40 mss:1448 cwnd:10 send 247.5Kbps rcv_rtt:34599.8 rcv_space:83984 ext = ext.split() for idx, entry in enumerate(ext): # mem:(r0,w0,f0,t0) or skmem:(r25216,rb172144,t0,tb23400,f3456,w0,o0,bl0) if entry.startswith("mem:(") or entry.startswith("skmem:("): if entry.startswith("skmem:("): tmp = entry[len("skmem:("):-1] elif entry.startswith("mem:("): tmp = entry[len("mem:("):-1] tmp = tmp.split(",") d["skmem"] = self.parse_skmem(tmp) ext.pop(idx) # wscale:%d,%d for idx, entry in enumerate(ext): if entry.startswith("wscale:"): vals = entry[len("wscale:"):].split(",") d["wscale"] = { "snd_wscale" : vals[0], "rcv_wscale" : vals[1] } ext.pop(idx) # rtt:%g/%g for idx, entry in enumerate(ext): if entry.startswith("rtt:"): vals = entry[len("rtt:"):].split("/") d["rtt"] = { "rtt" : vals[0], "rttvar" : vals[1] } ext.pop(idx) # retrans:%u/%u for idx, entry in enumerate(ext): if entry.startswith("retrans:"): vals = entry[len("retrans:"):].split("/") d["retrans"] = { "retrans" : vals[0], "total_retrans" : vals[1] } ext.pop(idx) # now parse "singles" - key:value pairs with exactly # one argument after the column singles = [ "rto", "ato", "mss", "cwnd", "ssthresh", \ "unacked", "lost", "sacked", "facket", \ "reordering", "rcv_rtt", "rcv_space" \ ] for single in singles: search_string = single + ":" for idx, entry in enumerate(ext): if entry.startswith(search_string): d[single] = entry[len(search_string):] ext.pop(idx) # search for "send %sbps" for idx, entry in enumerate(ext): if entry == "send": d["send"] = ext[idx + 1] ext.pop(idx + 1) ext.pop(idx) # any unhandled flags unhandled_flags = None if len(ext) > 0: d["unhandled_flags"] = ','.join(ext) self.logger.debug("unhandled ss(8) data %s" % (d["unhandled_flags"])) return d def account_connection(self, std, ext): new_data = dict() std_data = self.parse_std_line(std) if not std_data: return ext_data = self.parse_ext_line(ext) if not ext_data: return # merge std and extended data new_data.update(std_data) new_data.update(ext_data) handle = self.connection_handle(std_data["local_addr"], std_data["peer_addr"]) snapshot = handle.snapshot() snapshot.data = new_data def process_data(self, ss_output): # split into lines, remove tabs and empty lines = ss_output.split('\n') lines = [line.strip() for line in lines] lines = filter(None, lines) # filter first line, starting with State lines = filter(lambda x:not x.startswith("State"), lines) no_lines = len(lines) if no_lines == 0: sys.stdout.write("No connections active\n") return if no_lines % 2 != 0: sys.stdout.write("Currupted data set\n") return for i in range(0, no_lines, 2): std = lines[i]; ext = lines[i + 1] self.account_connection(std, ext) def initialize(self): if not sys.platform.startswith('linux'): sys.stderr.write("SocketStatistics only supported under Linux - sorry\n") sys.exit(ExitCodes.EXIT_PLATFORM) self.timeframe_start = None self.timeframe_end = None self.parse_local_options() self.db = dict() self.sleep_time = 1.0 / self.opts.sampling_rate def prepare_gnuplot_options(self): if not self.opts.gnuplotoptions: self.opts.gnuplotoptions = dict() return options = self.opts.gnuplotoptions.split(',') self.opts.gnuplotoptions = dict() for option in options: if option == "no-title" or option == "notitle": self.opts.gnuplotoptions["no-title"] = True continue if option.startswith("title="): title_token = option.split('=') if len(title_token) != 2: raise ArgumentException("Gnuplot title must be in " "form: title=\"New Title\"") self.opts.gnuplotoptions["title"] = title_token[1] continue if option.startswith("size-ratio="): title_token = option.split('=') if len(title_token) != 2: raise ArgumentException("Gnuplot size-ratio must be in " "form: size-ration=0.4") self.opts.gnuplotoptions["size-ratio"] = float(title_token[1]) if (not self.opts.gnuplotoptions["size-ratio"] > 0.0 and not self.opts.gnuplotoptions["size-ratio"] < 1.0): raise ArgumentException("Gnuplot size-ratio must be in " "range 0.01 to 0.99") continue # unknown options, raise error raise ArgumentException("Unknown gnuplot option: %s" % (option)) # sanity check if ("no-title" in self.opts.gnuplotoptions and "title" in self.opts.gnuplotoptions): raise ArgumentException("Gnuplot title AND no-title options are not allowed") def check_options(self): if not self.opts.outputdir: self.logger.error("No output directory specified: --output-dir <directory>") sys.exit(ExitCodes.EXIT_CMD_LINE) if not os.path.exists(self.opts.outputdir): self.logger.error("Not a valid directory: \"%s\"" % (self.opts.outputdir)) sys.exit(ExitCodes.EXIT_CMD_LINE) def parse_local_options(self): self.width = self.height = 0 parser = optparse.OptionParser() parser.usage = "%prog socketstatistic [options]" parser.add_option( "-v", "--loglevel", dest="loglevel", default=None, type="string", help="set the loglevel (info, debug, warning, error)") parser.add_option( "-o", "--output-dir", dest="outputdir", default=None, type="string", help="specify the output directory") parser.add_option( "-s", "--sampling-rate", dest="sampling_rate", default=1, type="float", help="How often per second should I take a measurement (default: 1Hz)") parser.add_option( "-f", "--force", dest="force", default=False, action="store_true", help="enforce directory overwrite") parser.add_option( "-g", "--gnuplot-options", dest="gnuplotoptions", default=None, type="string", help="options for gnuplot, comma separated list: notitle, " "title=\"<newtitle>\", size-ratio=<float>") self.opts, args = parser.parse_args(sys.argv[0:]) self.set_opts_logevel() self.captcp.print_welcome() self.prepare_gnuplot_options() self.check_options() def process_final(self): self.logger.warning("Sampling rate: %f Hz" % (self.opts.sampling_rate)) self.logger.warning("Start capturing socket data, interrupt process with CTRL-C") try: while True: data = self.execute_ss() self.process_data(data) time.sleep(self.sleep_time) except KeyboardInterrupt: self.logger.warning("\r# SIGINT received, please wait to generate data (this may take a while)") self.write_db() self.logger.warning("Data generated in %s/" % (self.opts.outputdir)) class Captcp: modes = { "geoip": [ "Geoip", "Show country/location information about peers" ], "payloadtimeport": [ "PayloadTimePortMod", "Show distribution of data over ports" ], "template": [ "TemplateMod", "Metamodule to generate template files for Gnuplot" ], "statistic": [ "StatisticMod", "Show statistic about all connections" ], "connection": [ "ConnectionAnalyzeMod", "Visualize all communicating peers" ], "flowgraph": [ "FlowGraphMod", "Visualize packet flow over time" ], "timesequence": [ "TimeSequenceMod", "Plot a Time-Sequence graph" ], "show": [ "ShowMod", "Tcpdump/tshark like mode" ], "throughput": [ "ThroughputMod", "Graph the throughput over time graph" ], "inflight": [ "InFlightMod", "Visualize all packets in flight and not ACKed" ], "spacing": [ "SpacingMod", "Time between packets and acks" ], "stacktrace": [ "StackTraceMod", "Hook into Linux Kernel to trace cwnd, ssthresh, ..." ], "sound": [ "SoundMod", "Play sound based on payload/ack packets" ], "animation": [ "ConnectionAnimationMod", "Generate animation (html) of packet flow" ], "socketstatistic": [ "SocketStatisticsMod", "Graph output (mem, rtt, ...) from ss(8) over time" ] } def __init__(self): self.captcp_starttime = datetime.datetime.today() self.setup_logging() self.pcap_filter = None self.pcap_file_path = False def setup_logging(self): ch = logging.StreamHandler() formatter = logging.Formatter("# %(message)s") ch.setFormatter(formatter) self.logger = logging.getLogger() self.logger.setLevel(logging.WARNING) self.logger.addHandler(ch) def which(self, program): for path in os.environ["PATH"].split(os.pathsep): path = path.strip('"') full_path = os.path.join(path, program) if os.path.isfile(full_path) and os.access(full_path, os.X_OK): return full_path return None def check_program(self, program): status = "FAILED" path = self.which(program["name"]) if path: status = "OK" else: path = "" required = "Required" if program["required"] else "Optional" help = "" if not program["help"] else program["help"] sys.stdout.write("%-6s %-10s Need: %7s Path: %-20s Help: %s\n" % (status, program["name"], required, path, help)) def check_environment(self): programs = [ # Required program section { "name":"make", "required":True, "os":None, "help":"Build environment" }, { "name":"epstopdf", "required":True, "os":None, "help":"Used to convert Gnuplot EPS output to PDF" }, { "name":"gnuplot", "required":True, "os":None, "help":"Main plotting program" }, # Optional programs { "name":"convert", "required":False, "os":None, "help":"Used to convert PDF to PNG" }, { "name":"ss", "required":False, "os":"linux", "help":"Required for socketstatistic module" } ] sys.stdout.write("Platform: %s\n" % (sys.platform)) major, minor, micro, releaselevel, serial = sys.version_info sys.stdout.write("Python: %s.%s.%s [releaselevel: %s, serial: %s]\n\n" % (major, minor, micro, releaselevel, serial)) sys.stdout.write("Check programs:\n") for program in programs: self.check_program(program) def print_version(self): sys.stdout.write("%s\n" % (__version__)) def print_usage(self): sys.stderr.write("Usage: captcp [-h | --help]" + " [--version]" + " <modulename> [<module-options>] <pcap-file>\n") def print_welcome(self): major, minor, micro, releaselevel, serial = sys.version_info self.logger.critical("captcp 2010-2013 Hagen Paul Pfeifer and others (c)") self.logger.critical("http://research.protocollabs.com/captcp/") self.logger.info("python: %s.%s.%s [releaselevel: %s, serial: %s]" % (major, minor, micro, releaselevel, serial)) def print_modules(self): for i in Captcp.modes.keys(): sys.stderr.write(" %-15s - %s\n" % (i, Captcp.modes[i][1])) def args_contains(self, argv, *cmds): for cmd in cmds: for arg in argv: if arg == cmd: return True return False def parse_global_otions(self): if len(sys.argv) <= 1: self.print_usage() sys.stderr.write("Available modules:\n") self.print_modules() return None # --version can be placed somewhere in the # command line and will evalutated always: it is # a global option if self.args_contains(sys.argv, "--version"): self.print_version() return None # -h | --help as first argument is treated special # and has other meaning as a submodule if self.args_contains(sys.argv[1:2], "-h", "--help"): self.print_usage() sys.stderr.write("Available modules:\n") self.print_modules() return None # -c | --check as first argument is treated special if self.args_contains(sys.argv[1:2], "-c", "--check"): self.check_environment() return None submodule = sys.argv[1].lower() if submodule not in Captcp.modes: self.print_usage() sys.stderr.write("Module \"%s\" not known, available modules are:\n" % (submodule)) self.print_modules() return None classname = Captcp.modes[submodule][0] return classname def check_pcap_filepath_sanity(self): statinfo = os.stat(self.pcap_file_path) if statinfo.st_size == 0: self.logger.error("PCAP file contains no data: %s - exiting" % (self.pcap_file_path)) return False return True def run(self): classtring = self.parse_global_otions() if not classtring: return 1 classinstance = globals()[classtring]() classinstance.register_captcp(self) classinstance.initialize() # there are other usages two (without pcap parsing) # We check here and if pcap_file_path is not true # then we assume a non-pcap module if self.pcap_file_path: if not self.check_pcap_filepath_sanity(): return 1 # parse the whole pcap file first pcap_parser = PcapParser(self.pcap_file_path, self.pcap_filter) pcap_parser.register_callback(classinstance.internal_pre_process_packet) self.logger.debug("call pre_process_packet [1/4]") pcap_parser.run() del pcap_parser self.logger.debug("call pre_process_final [2/4]") classinstance.pre_process_final() # and finally print all relevant stuff pcap_parser = PcapParser(self.pcap_file_path, self.pcap_filter) pcap_parser.register_callback(classinstance.process_packet) self.logger.debug("call process_packet [3/4]") pcap_parser.run() del pcap_parser self.logger.debug("call pre_process_final [4/4]") ret = classinstance.process_final() time_diff = datetime.datetime.today() - self.captcp_starttime time_diff_s = float(time_diff.seconds) + time_diff.microseconds / \ 1E6 + time_diff.days * 86400 self.logger.info("processing duration: %.4f seconds" % (time_diff_s)) return ret if __name__ == "__main__": try: captcp = Captcp() sys.exit(captcp.run()) except KeyboardInterrupt: sys.stderr.write("SIGINT received, exiting\n")