1
0

salt-tcpdump.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. '''
  4. Author: Volker Schwicking, vs@heg.com
  5. Salt tcpdumper to visualize whats happening network-wise on
  6. the salt-master. It uses pcapy and inspects all incoming networks
  7. packets and filters only the ones relevant to salt-communication.
  8. based on: http://oss.coresecurity.com/projects/pcapy.html
  9. $ salt-tcpdump.py -n 2
  10. Will print the overall tcp-status of tcp-connections to salts
  11. default ports in a two second interval.
  12. $ salt-tcpdump.py -I -n 2
  13. Will print the number of IPs making new connections to salts
  14. default ports.
  15. $ salt-tcpdump.py -I -n 2 -i eth1
  16. Same as before but on eth1 instead of the default eth0.
  17. Rough equivalents to this script could be:
  18. For Port 4505
  19. tcpdump "tcp[tcpflags] & tcp-syn != 0" and port 4505 and "tcp[tcpflags] & tcp-ack == 0"
  20. For Port 4506
  21. tcpdump "tcp[tcpflags] & tcp-syn != 0" and port 4506 and "tcp[tcpflags] & tcp-ack == 0"
  22. '''
  23. # pylint: disable=resource-leakage
  24. # Import Python Libs
  25. from __future__ import absolute_import, print_function
  26. import socket
  27. from struct import unpack
  28. import pcapy # pylint: disable=import-error,3rd-party-module-not-gated
  29. import sys
  30. import argparse # pylint: disable=minimum-python-version
  31. import time
  32. class ArgParser(object):
  33. '''
  34. Simple Argument-Parser class
  35. '''
  36. def __init__(self):
  37. '''
  38. Init the Parser
  39. '''
  40. self.main_parser = argparse.ArgumentParser()
  41. self.add_args()
  42. def add_args(self):
  43. '''
  44. Add new arguments
  45. '''
  46. self.main_parser.add_argument('-i',
  47. type=str,
  48. default='eth0',
  49. dest='iface',
  50. required=False,
  51. help=('the interface to dump the'
  52. 'master runs on(default:eth0)'))
  53. self.main_parser.add_argument('-n',
  54. type=int,
  55. default=5,
  56. dest='ival',
  57. required=False,
  58. help=('interval for printing stats '
  59. '(default:5)'))
  60. self.main_parser.add_argument('-I',
  61. type=bool,
  62. default=False,
  63. const=True,
  64. nargs='?',
  65. dest='only_ip',
  66. required=False,
  67. help=('print unique IPs making new '
  68. 'connections with SYN set'))
  69. def parse_args(self):
  70. '''
  71. parses and returns the given arguments in a namespace object
  72. '''
  73. return self.main_parser.parse_args()
  74. class PCAPParser(object):
  75. '''
  76. parses a network packet on given device and
  77. returns source, target, source_port and dest_port
  78. '''
  79. def __init__(self, iface):
  80. self.iface = iface
  81. def run(self):
  82. '''
  83. main loop for the packet-parser
  84. '''
  85. # open device
  86. # Arguments here are:
  87. # device
  88. # snaplen (maximum number of bytes to capture _per_packet_)
  89. # promiscious mode (1 for true)
  90. # timeout (in milliseconds)
  91. cap = pcapy.open_live(self.iface, 65536, 1, 0)
  92. count = 0
  93. l_time = None
  94. while 1:
  95. packet_data = {
  96. 'ip': {},
  97. 'tcp': {}
  98. }
  99. (header, packet) = cap.next() # pylint: disable=incompatible-py3-code
  100. eth_length, eth_protocol = self.parse_ether(packet)
  101. # Parse IP packets, IP Protocol number = 8
  102. if eth_protocol == 8:
  103. # Parse IP header
  104. # take first 20 characters for the ip header
  105. version_ihl, version, ihl, iph_length, ttl, protocol, s_addr, d_addr = self.parse_ip(packet, eth_length)
  106. packet_data['ip']['s_addr'] = s_addr
  107. packet_data['ip']['d_addr'] = d_addr
  108. # TCP protocol
  109. if protocol == 6:
  110. source_port, dest_port, flags, data = self.parse_tcp(packet, iph_length, eth_length)
  111. packet_data['tcp']['d_port'] = dest_port
  112. packet_data['tcp']['s_port'] = source_port
  113. packet_data['tcp']['flags'] = flags
  114. packet_data['tcp']['data'] = data
  115. yield packet_data
  116. def parse_ether(self, packet):
  117. '''
  118. parse ethernet_header and return size and protocol
  119. '''
  120. eth_length = 14
  121. eth_header = packet[:eth_length]
  122. eth = unpack('!6s6sH', eth_header)
  123. eth_protocol = socket.ntohs(eth[2])
  124. return eth_length, eth_protocol
  125. def parse_ip(self, packet, eth_length):
  126. '''
  127. parse ip_header and return all ip data fields
  128. '''
  129. # Parse IP header
  130. # take first 20 characters for the ip header
  131. ip_header = packet[eth_length:20+eth_length]
  132. # now unpack them:)
  133. iph = unpack('!BBHHHBBH4s4s', ip_header)
  134. version_ihl = iph[0]
  135. version = version_ihl >> 4
  136. ihl = version_ihl & 0xF
  137. iph_length = ihl * 4
  138. ttl = iph[5]
  139. protocol = iph[6]
  140. s_addr = socket.inet_ntoa(iph[8])
  141. d_addr = socket.inet_ntoa(iph[9])
  142. return [version_ihl,
  143. version,
  144. ihl,
  145. iph_length,
  146. ttl,
  147. protocol,
  148. s_addr,
  149. d_addr]
  150. def parse_tcp(self, packet, iph_length, eth_length):
  151. '''
  152. parse tcp_data and return source_port,
  153. dest_port and actual packet data
  154. '''
  155. p_len = iph_length + eth_length
  156. tcp_header = packet[p_len:p_len+20]
  157. # now unpack them:)
  158. tcph = unpack('!H HLLBBHHH', tcp_header)
  159. # H H L L B B H H H
  160. # 2b 2b 4b 4b 1b 1b 2b 2b 2b
  161. # sport dport seq ack res flags win chk up
  162. # (22, 36513, 3701969065, 2346113113, 128, 24, 330, 33745, 0)
  163. source_port = tcph[0]
  164. dest_port = tcph[1]
  165. sequence = tcph[2]
  166. acknowledgement = tcph[3]
  167. doff_reserved = tcph[4]
  168. tcph_length = doff_reserved >> 4
  169. tcp_flags = tcph[5]
  170. h_size = eth_length + iph_length + tcph_length * 4
  171. data_size = len(packet) - h_size
  172. data = packet[h_size:]
  173. return source_port, dest_port, tcp_flags, data
  174. class SaltNetstat(object):
  175. '''
  176. Reads /proc/net/tcp and returns all connections
  177. '''
  178. def proc_tcp(self):
  179. '''
  180. Read the table of tcp connections & remove header
  181. '''
  182. with open('/proc/net/tcp', 'r') as tcp_f:
  183. content = tcp_f.readlines()
  184. content.pop(0)
  185. return content
  186. def hex2dec(self, hex_s):
  187. '''
  188. convert hex to dezimal
  189. '''
  190. return str(int(hex_s, 16))
  191. def ip(self, hex_s):
  192. '''
  193. convert into readable ip
  194. '''
  195. ip = [(self.hex2dec(hex_s[6:8])),
  196. (self.hex2dec(hex_s[4:6])),
  197. (self.hex2dec(hex_s[2:4])),
  198. (self.hex2dec(hex_s[0:2]))]
  199. return '.'.join(ip)
  200. def remove_empty(self, array):
  201. '''
  202. create new list without empty entries
  203. '''
  204. return [x for x in array if x != '']
  205. def convert_ip_port(self, array):
  206. '''
  207. hex_ip:hex_port to str_ip:str_port
  208. '''
  209. host, port = array.split(':')
  210. return self.ip(host), self.hex2dec(port)
  211. def run(self):
  212. '''
  213. main loop for netstat
  214. '''
  215. while 1:
  216. ips = {
  217. 'ips/4505': {},
  218. 'ips/4506': {}
  219. }
  220. content = self.proc_tcp()
  221. for line in content:
  222. line_array = self.remove_empty(line.split(' '))
  223. l_host, l_port = self.convert_ip_port(line_array[1])
  224. r_host, r_port = self.convert_ip_port(line_array[2])
  225. if l_port == '4505':
  226. if r_host not in ips['ips/4505']:
  227. ips['ips/4505'][r_host] = 0
  228. ips['ips/4505'][r_host] += 1
  229. if l_port == '4506':
  230. if r_host not in ips['ips/4506']:
  231. ips['ips/4506'][r_host] = 0
  232. ips['ips/4506'][r_host] += 1
  233. yield (len(ips['ips/4505']), len(ips['ips/4506']))
  234. time.sleep(0.5)
  235. def filter_new_cons(packet):
  236. '''
  237. filter packets by there tcp-state and
  238. returns codes for specific states
  239. '''
  240. flags = []
  241. TCP_FIN = 0x01
  242. TCP_SYN = 0x02
  243. TCP_RST = 0x04
  244. TCP_PSH = 0x08
  245. TCP_ACK = 0x10
  246. TCP_URG = 0x20
  247. TCP_ECE = 0x40
  248. TCP_CWK = 0x80
  249. if packet['tcp']['flags'] & TCP_FIN:
  250. flags.append('FIN')
  251. elif packet['tcp']['flags'] & TCP_SYN:
  252. flags.append('SYN')
  253. elif packet['tcp']['flags'] & TCP_RST:
  254. flags.append('RST')
  255. elif packet['tcp']['flags'] & TCP_PSH:
  256. flags.append('PSH')
  257. elif packet['tcp']['flags'] & TCP_ACK:
  258. flags.append('ACK')
  259. elif packet['tcp']['flags'] & TCP_URG:
  260. flags.append('URG')
  261. elif packet['tcp']['flags'] & TCP_ECE:
  262. flags.append('ECE')
  263. elif packet['tcp']['flags'] & TCP_CWK:
  264. flags.append('CWK')
  265. else:
  266. print("UNKNOWN PACKET")
  267. if packet['tcp']['d_port'] == 4505:
  268. # track new connections
  269. if 'SYN' in flags and len(flags) == 1:
  270. return 10
  271. # track closing connections
  272. elif 'FIN' in flags:
  273. return 12
  274. elif packet['tcp']['d_port'] == 4506:
  275. # track new connections
  276. if 'SYN' in flags and len(flags) == 1:
  277. return 100
  278. # track closing connections
  279. elif 'FIN' in flags:
  280. return 120
  281. # packet does not match requirements
  282. else:
  283. return None
  284. def main():
  285. '''
  286. main loop for whole script
  287. '''
  288. # passed parameters
  289. args = vars(ArgParser().parse_args())
  290. # reference timer for printing in intervals
  291. r_time = 0
  292. # the ports we want to monitor
  293. ports = [4505, 4506]
  294. print("Sniffing device {0}".format(args['iface']))
  295. stat = {
  296. '4506/new': 0,
  297. '4506/est': 0,
  298. '4506/fin': 0,
  299. '4505/new': 0,
  300. '4505/est': 0,
  301. '4505/fin': 0,
  302. 'ips/4505': 0,
  303. 'ips/4506': 0
  304. }
  305. if args['only_ip']:
  306. print(
  307. 'IPs making new connections '
  308. '(ports:{0}, interval:{1})'.format(ports,
  309. args['ival'])
  310. )
  311. else:
  312. print(
  313. 'Salt-Master Network Status '
  314. '(ports:{0}, interval:{1})'.format(ports,
  315. args['ival'])
  316. )
  317. try:
  318. while 1:
  319. s_time = int(time.time())
  320. packet = next(PCAPParser(args['iface']).run())
  321. p_state = filter_new_cons(packet)
  322. ips_auth = []
  323. ips_push = []
  324. # new connection to 4505
  325. if p_state == 10:
  326. stat['4505/new'] += 1
  327. if packet['ip']['s_addr'] not in ips_auth:
  328. ips_auth.append(packet['ip']['s_addr'])
  329. # closing connection to 4505
  330. elif p_state == 12:
  331. stat['4505/fin'] += 1
  332. # new connection to 4506
  333. elif p_state == 100:
  334. stat['4506/new'] += 1
  335. if packet['ip']['s_addr'] not in ips_push:
  336. ips_push.append(packet['ip']['s_addr'])
  337. # closing connection to 4506
  338. elif p_state == 120:
  339. stat['4506/fin'] += 1
  340. # get the established connections to 4505 and 4506
  341. # these would only show up in tcpdump if data is transferred
  342. # but then with different flags (PSH, etc.)
  343. stat['4505/est'], stat['4506/est'] = next(SaltNetstat().run())
  344. # print only in intervals
  345. if (s_time % args['ival']) == 0:
  346. # prevent printing within the same second
  347. if r_time != s_time:
  348. if args['only_ip']:
  349. msg = 'IPs/4505: {0}, IPs/4506: {1}'.format(len(ips_auth),
  350. len(ips_push))
  351. else:
  352. msg = "4505=>[ est: {0}, ".format(stat['4505/est'])
  353. msg += "new: {0}/s, ".format(stat['4505/new'] / args['ival'])
  354. msg += "fin: {0}/s ] ".format(stat['4505/fin'] / args['ival'])
  355. msg += " 4506=>[ est: {0}, ".format(stat['4506/est'])
  356. msg += "new: {0}/s, ".format(stat['4506/new'] / args['ival'])
  357. msg += "fin: {0}/s ]".format(stat['4506/fin'] / args['ival'])
  358. print(msg)
  359. # reset the so far collected stats
  360. for item in stat:
  361. stat[item] = 0
  362. r_time = s_time
  363. except KeyboardInterrupt:
  364. sys.exit(1)
  365. if __name__ == "__main__":
  366. main()