1
0

salt-tcpdump.py 13 KB

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