123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447 |
- #!/usr/bin/python
- # -*- coding: utf-8 -*-
- '''
- 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"
- '''
- # pylint: disable=resource-leakage
- # Import Python Libs
- from __future__ import absolute_import, print_function
- import socket
- from struct import unpack
- import pcapy # pylint: disable=import-error,3rd-party-module-not-gated
- import sys
- import argparse # pylint: disable=minimum-python-version
- import time
- class ArgParser(object):
- '''
- Simple Argument-Parser class
- '''
- def __init__(self):
- '''
- Init the Parser
- '''
- self.main_parser = argparse.ArgumentParser()
- self.add_args()
- def add_args(self):
- '''
- Add new arguments
- '''
- self.main_parser.add_argument('-i',
- type=str,
- default='eth0',
- dest='iface',
- required=False,
- help=('the interface to dump the'
- 'master runs on(default:eth0)'))
- self.main_parser.add_argument('-n',
- type=int,
- default=5,
- dest='ival',
- required=False,
- help=('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 parse_args(self):
- '''
- parses and returns the given arguments in a namespace object
- '''
- 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):
- '''
- main loop for the packet-parser
- '''
- # 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() # pylint: disable=incompatible-py3-code
- 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
- '''
- p_len = iph_length + eth_length
- tcp_header = packet[p_len:p_len+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):
- '''
- Reads /proc/net/tcp and returns all connections
- '''
- def proc_tcp(self):
- '''
- Read the table of tcp connections & remove header
- '''
- with open('/proc/net/tcp', 'r') as tcp_f:
- content = tcp_f.readlines()
- content.pop(0)
- return content
- def hex2dec(self, hex_s):
- '''
- convert hex to dezimal
- '''
- return str(int(hex_s, 16))
- def ip(self, hex_s):
- '''
- convert into readable ip
- '''
- ip = [(self.hex2dec(hex_s[6:8])),
- (self.hex2dec(hex_s[4:6])),
- (self.hex2dec(hex_s[2:4])),
- (self.hex2dec(hex_s[0:2]))]
- return '.'.join(ip)
- def remove_empty(self, array):
- '''
- create new list without empty entries
- '''
- return [x for x in array if x != '']
- def convert_ip_port(self, array):
- '''
- hex_ip:hex_port to str_ip:str_port
- '''
- host, port = array.split(':')
- return self.ip(host), self.hex2dec(port)
- def run(self):
- '''
- main loop for netstat
- '''
- 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
- def main():
- '''
- main loop for whole script
- '''
- # passed parameters
- args = vars(ArgParser().parse_args())
- # 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']))
- stat = {
- '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 = next(PCAPParser(args['iface']).run())
- p_state = filter_new_cons(packet)
- ips_auth = []
- ips_push = []
- # new connection to 4505
- if p_state == 10:
- stat['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:
- stat['4505/fin'] += 1
- # new connection to 4506
- elif p_state == 100:
- stat['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:
- stat['4506/fin'] += 1
- # get the established connections to 4505 and 4506
- # these would only show up in tcpdump if data is transferred
- # but then with different flags (PSH, etc.)
- stat['4505/est'], stat['4506/est'] = next(SaltNetstat().run())
- # 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(stat['4505/est'])
- msg += "new: {0}/s, ".format(stat['4505/new'] / args['ival'])
- msg += "fin: {0}/s ] ".format(stat['4505/fin'] / args['ival'])
- msg += " 4506=>[ est: {0}, ".format(stat['4506/est'])
- msg += "new: {0}/s, ".format(stat['4506/new'] / args['ival'])
- msg += "fin: {0}/s ]".format(stat['4506/fin'] / args['ival'])
- print(msg)
- # reset the so far collected stats
- for item in stat:
- stat[item] = 0
- r_time = s_time
- except KeyboardInterrupt:
- sys.exit(1)
- if __name__ == "__main__":
- main()
|