add tcp-dumper for visualzing salt-master network activity

This commit is contained in:
vs 2014-08-15 12:11:50 +02:00
parent 27782ee6cc
commit 0ad1f016ed

391
tests/salt-tcpdump.py Executable file
View file

@ -0,0 +1,391 @@
#!/usr/bin/python
'''
Author: Volker Schwicking, vs@heg.com
Salt tcpdumper to visualize whats happening network-wise on
the salt-master. It uses pcapy and inspects all incoming networks
packets and filters only the ones relevant to salt-communication.
based on: http://oss.coresecurity.com/projects/pcapy.html
$ salt-tcpdump.py -n 2
Will print the overall tcp-status of tcp-connections to salts
default ports in a two second interval.
$ salt-tcpdump.py -I -n 2
Will print the number of IPs making new connections to salts
default ports.
$ salt-tcpdump.py -I -n 2 -i eth1
Same as before but on eth1 instead of the default eth0.
Rough equivalents to this script could be:
For Port 4505
tcpdump "tcp[tcpflags] & tcp-syn != 0" and port 4505 and "tcp[tcpflags] & tcp-ack == 0"
For Port 4506
tcpdump "tcp[tcpflags] & tcp-syn != 0" and port 4506 and "tcp[tcpflags] & tcp-ack == 0"
'''
import socket
from struct import *
import datetime
import pcapy
import sys
import argparse
import time
class ArgParser(object):
def __init__(self):
self.main_parser = argparse.ArgumentParser()
self.addArgs()
def addArgs(self):
self.main_parser.add_argument('-i',
type=str,
default='eth0',
dest='iface',
required=False,
help='the interface to dump (default:eth0)')
self.main_parser.add_argument('-n',
type=int,
default=5,
dest='ival',
required=False,
help='the interval for printing stats (default:5)')
self.main_parser.add_argument('-I',
type=bool,
default=False,
const=True,
nargs='?',
dest='only_ip',
required=False,
help='print unique IPs making new connections with SYN set')
def parseArgs(self):
return self.main_parser.parse_args()
class PCAPParser(object):
'''
parses a network packet on given device and
returns source, target, source_port and dest_port
'''
def __init__(self, iface):
self.iface = iface
def run(self):
# open device
# Arguments here are:
# device
# snaplen (maximum number of bytes to capture _per_packet_)
# promiscious mode (1 for true)
# timeout (in milliseconds)
cap = pcapy.open_live(self.iface, 65536 , 1 , 0)
count = 0
l_time = None
while(1) :
packet_data = {
'ip' : {},
'tcp' : {}
}
(header, packet) = cap.next()
eth_length, eth_protocol = self.parse_ether(packet)
# Parse IP packets, IP Protocol number = 8
if eth_protocol == 8 :
#Parse IP header
#take first 20 characters for the ip header
version_ihl, version, ihl, iph_length, ttl, protocol, s_addr, d_addr = self.parse_ip(packet, eth_length)
packet_data['ip']['s_addr'] = s_addr
packet_data['ip']['d_addr'] = d_addr
#TCP protocol
if protocol == 6 :
source_port, dest_port, flags, data = self.parse_tcp(packet, iph_length, eth_length)
packet_data['tcp']['d_port'] = dest_port
packet_data['tcp']['s_port'] = source_port
packet_data['tcp']['flags'] = flags
packet_data['tcp']['data'] = data
yield packet_data
def parse_ether(self, packet):
'''
parse ethernet_header and return size and protocol
'''
eth_length = 14
eth_header = packet[:eth_length]
eth = unpack('!6s6sH' , eth_header)
eth_protocol = socket.ntohs(eth[2])
return eth_length, eth_protocol
def parse_ip(self, packet, eth_length):
'''
parse ip_header and return all ip data fields
'''
#Parse IP header
#take first 20 characters for the ip header
ip_header = packet[eth_length:20+eth_length]
#now unpack them :)
iph = unpack('!BBHHHBBH4s4s' , ip_header)
version_ihl = iph[0]
version = version_ihl >> 4
ihl = version_ihl & 0xF
iph_length = ihl * 4
ttl = iph[5]
protocol = iph[6]
s_addr = socket.inet_ntoa(iph[8]);
d_addr = socket.inet_ntoa(iph[9]);
return [version_ihl, version, ihl, iph_length, ttl, protocol, s_addr, d_addr]
def parse_tcp(self, packet, iph_length, eth_length):
'''
parse tcp_data and return source_port,
dest_port and actual packet data
'''
t = iph_length + eth_length
tcp_header = packet[t:t+20]
#now unpack them :)
tcph = unpack('!H HLLBBHHH' , tcp_header)
# H H L L B B H H H
# 2b 2b 4b 4b 1b 1b 2b 2b 2b
# sport dport seq ack res flags win chk up
# (22, 36513, 3701969065, 2346113113, 128, 24, 330, 33745, 0)
source_port = tcph[0]
dest_port = tcph[1]
sequence = tcph[2]
acknowledgement = tcph[3]
doff_reserved = tcph[4]
tcph_length = doff_reserved >> 4
tcp_flags = tcph[5]
h_size = eth_length + iph_length + tcph_length * 4
data_size = len(packet) - h_size
data = packet[h_size:]
return source_port, dest_port, tcp_flags, data
class SaltNetstat(object):
def proc_tcp(self):
'''
Read the table of tcp connections & remove header
'''
with open('/proc/net/tcp', 'r') as f:
content = f.readlines()
content.pop(0)
return content
def hex2dec(self, s):
return str(int(s, 16))
def ip(self, s):
ip = [(self.hex2dec(s[6:8])),(self.hex2dec(s[4:6])),(self.hex2dec(s[2:4])),(self.hex2dec(s[0:2]))]
return '.'.join(ip)
def remove_empty(self, array):
return [x for x in array if x != '']
def convert_ip_port(self, array):
host, port = array.split(':')
return self.ip(host), self.hex2dec(port)
def run(self):
while(1):
ips = {
'ips/4505' : {},
'ips/4506' : {}
}
content = self.proc_tcp()
for line in content:
line_array = self.remove_empty(line.split(' '))
l_host, l_port = self.convert_ip_port(line_array[1])
r_host, r_port = self.convert_ip_port(line_array[2])
if l_port == '4505':
if r_host not in ips['ips/4505']:
ips['ips/4505'][r_host] = 0
ips['ips/4505'][r_host] += 1
if l_port == '4506':
if r_host not in ips['ips/4506']:
ips['ips/4506'][r_host] = 0
ips['ips/4506'][r_host] += 1
yield (len(ips['ips/4505']), len(ips['ips/4506']))
time.sleep(0.5)
def filter_new_cons(packet):
'''
filter packets by there tcp-state and
returns codes for specific states
'''
FLAGS = []
TCP_FIN = 0x01
TCP_SYN = 0x02
TCP_RST = 0x04
TCP_PSH = 0x08
TCP_ACK = 0x10
TCP_URG = 0x20
TCP_ECE = 0x40
TCP_CWK = 0x80
if packet['tcp']['flags'] & TCP_FIN:
FLAGS.append('FIN')
elif packet['tcp']['flags'] & TCP_SYN:
FLAGS.append('SYN')
elif packet['tcp']['flags'] & TCP_RST:
FLAGS.append('RST')
elif packet['tcp']['flags'] & TCP_PSH:
FLAGS.append('PSH')
elif packet['tcp']['flags'] & TCP_ACK:
FLAGS.append('ACK')
elif packet['tcp']['flags'] & TCP_URG:
FLAGS.append('URG')
elif packet['tcp']['flags'] & TCP_ECE:
FLAGS.append('ECE')
elif packet['tcp']['flags'] & TCP_CWK:
FLAGS.append('CWK')
else:
print "UNKNOWN PACKET"
if packet['tcp']['d_port'] == 4505:
# track new connections
if 'SYN' in FLAGS and len(FLAGS) == 1:
return 10
# track closing connections
elif 'FIN' in FLAGS:
return 12
elif packet['tcp']['d_port'] == 4506:
# track new connections
if 'SYN' in FLAGS and len(FLAGS) == 1:
return 100
# track closing connections
elif 'FIN' in FLAGS:
return 120
# packet does not match requirements
else:
return None
if __name__ == "__main__":
# passed parameters
args = vars(ArgParser().parseArgs())
# reference timer for printing in intervals
r_time = 0
# the ports we want to monitor
ports = [4505, 4506]
print "Sniffing device {0}".format(args['iface'])
stats = {
'ips/4506' : [],
'4506/new' : 0,
'4506/est' : 0,
'4506/fin' : 0,
'4505/new' : 0,
'4505/est' : 0,
'4505/fin' : 0,
'ips/4505' : 0,
'ips/4506' : 0
}
if args['only_ip']:
print "IPs making new connections (ports:{0}, interval:{1})".format(ports,
args['ival'])
else:
print "Salt-Master Network Status (ports:{0}, interval:{1})".format(ports,
args['ival'])
try:
while(1):
s_time = int(time.time())
packet = PCAPParser(args['iface']).run().next()
p_state = filter_new_cons(packet)
ips_auth = []
ips_push = []
# new connection to 4505
if p_state == 10:
stats['4505/new'] += 1
if packet['ip']['s_addr'] not in ips_auth:
ips_auth.append(packet['ip']['s_addr'])
# closing connection to 4505
elif p_state == 12:
stats['4505/fin'] += 1
# new connection to 4506
elif p_state == 100:
stats['4506/new'] += 1
if packet['ip']['s_addr'] not in ips_push:
ips_push.append(packet['ip']['s_addr'])
# closing connection to 4506
elif p_state == 120:
stats['4506/fin'] += 1
# get the established connections to 4505 and 4506
# these would only show up in tcpdump if data is transferred
stats['4505/est'], stats['4506/est'] = SaltNetstat().run().next()
# print only in intervals
if s_time % args['ival']) == 0:
# prevent printing within the same second
if r_time != s_time:
if args['only_ip']:
msg = 'IPs/4505: {0}, IPs/4506: {1}'.format(len(ips_auth),
len(ips_push))
else:
msg = "4505=>[ est: {0}, ".format(stats['4505/est'])
msg += "new: {0}/s, ".format(stats['4505/new'] / args['ival'])
msg += "fin: {0}/s ] ".format(stats['4505/fin'] / args['ival'])
msg += " 4506=>[ est: {0}, ".format(stats['4506/est'])
msg += "new: {0}/s, ".format(stats['4506/new'] / args['ival'])
msg += "fin: {0}/s ]".format(stats['4506/fin'] / args['ival'])
print msg
for item in stats:
stats[item] = 0
r_time = s_time
except KeyboardInterrupt:
sys.exit(1)