visualizer.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. """
  2. Network traffic visualizer framework using Graph Tool and GTK+
  3. Requirements:
  4. graph-tool (>=v2.2.31)
  5. gtk+ libraries (>=3.12.2)
  6. Graph tool sources:
  7. http://graph-tool.skewed.de/download
  8. http://graph-tool.skewed.de/static/doc/quickstart.html
  9. Installation Note:
  10. Compiling Graph tool needs a whole lot of memory (>4GiB). On a 32Bit system you
  11. are encouraged to use the newst gcc compiler in order to minimize memory
  12. usage (gcc v4.8.3 worked on graph-tool v2.2.31)
  13. """
  14. import logging
  15. import time
  16. import random
  17. import threading
  18. logger = logging.getLogger("pypacker")
  19. try:
  20. import graph_tool
  21. from graph_tool import Graph, Vertex, Edge, GraphView
  22. from graph_tool.draw import arf_layout, sfdp_layout, fruchterman_reingold_layout, radial_tree_layout, GraphWindow
  23. from gi.repository import Gtk, GObject
  24. except Exception as e:
  25. logger.warning("Could not find graph-tool and/or Gtk+ libs which are needed for visualizer")
  26. logger.exception(e)
  27. _key_listener = []
  28. def key_press_event(self, widget, event):
  29. r"""Handle key press."""
  30. # print(event.keyval)
  31. if event.keyval == 114:
  32. self.fit_to_window()
  33. self.regenerate_surface(timeout=50)
  34. self.queue_draw()
  35. elif event.keyval == 115:
  36. self.reset_layout()
  37. elif event.keyval == 97:
  38. self.apply_transform()
  39. elif event.keyval == 112:
  40. if self.picked is False:
  41. self.init_picked()
  42. else:
  43. self.picked = False
  44. self.selected.fa = False
  45. self.vertex_matrix = None
  46. self.queue_draw()
  47. elif event.keyval == 0x7a:
  48. if isinstance(self.picked, PropertyMap):
  49. u = GraphView(self.g, vfilt=self.picked)
  50. self.fit_to_window(g=u)
  51. self.regenerate_surface(timeout=50)
  52. self.queue_draw()
  53. # key "t": call listener callbacks and update layout
  54. elif event.keyval == 116:
  55. # print("resetting positions")
  56. for l in _key_listener:
  57. l()
  58. self.apply_transform()
  59. self.reset_layout()
  60. return True
  61. # add new listener: press "t" to reorder node effectively
  62. graph_tool.draw.gtk_draw.GraphWidget.key_press_event = key_press_event
  63. def config_cb_default(packet, vertex_src, vertex_dst, edge, vertexprop_dict, edgeprop_dict):
  64. """
  65. Default configuration callback.
  66. """
  67. vertexprop_dict["text"][vertex_src] = "N"
  68. if vertex_dst is not None:
  69. vertexprop_dict["text"][vertex_dst] = "N"
  70. edgeprop_dict["text"][edge] = "E"
  71. def __getattr__autocreate(self, name):
  72. """
  73. Set default values for unknown variables having names suffix like:
  74. _n = 0
  75. _s = ""
  76. _b = False
  77. _y = b""
  78. _l = []
  79. _d = {}
  80. _e = set()
  81. """
  82. defaults = {"_n": 0, "_s": "", "_b": False, "_y": b"", "_l": [], "_d": {}, "_e": set()}
  83. try:
  84. value = defaults[name[-2:]]
  85. # logger.debug("suffix: %s" % value)
  86. except:
  87. raise AttributeError()
  88. object.__setattr__(self, name, value)
  89. return value
  90. # Allow auto creation of variables for convenience.
  91. # This comes in handy when implementing "config_cb"
  92. Vertex.__getattr__ = __getattr__autocreate
  93. Edge.__getattr__ = __getattr__autocreate
  94. class Visualizer(object):
  95. """
  96. Vizualizer using graph-tool to visualize nodes in a network.
  97. Note: xxx_cb is callback returning values stated in the desriptions.
  98. Livecycle: stopped - started (paused <-> running) - stopped (terminated)
  99. iterable -- an object which is iterable and returns packets, raises StopIteration on EOF
  100. src_dst_cb -- returns list like [source, destination] or [source, None] eg ["127.0.0.1", "127.0.0.2"].
  101. Destination can be set to None eg for broadcast-packets. If both are not None an edge will be
  102. added automatically. Source/destination must uniquely identify a node.
  103. Callback-structure: fct(packet)
  104. config_cb -- updates a dict representing the current node-config
  105. Callback-structure: fct(packet, vertex_src, vertex_dst, edge, vertex_props, edge_props).
  106. In order to store additional data (like number of total packets from this source) save
  107. them in the vertex-object itself like "vertex_src.my_data=123".
  108. additional_vertexprops -- additional vertex properties to be added via [name, format, defaultvalue]
  109. Property definitions: http://graph-tool.skewed.de/static/doc/draw.html
  110. additional_edgeprops -- additional properties to be added via [name, format, defaultvalue]
  111. Property definitions: http://graph-tool.skewed.de/static/doc/draw.html
  112. node_timeout -- timeout until node vanishes in seconds, default is 60
  113. #update_interval -- update interval for drawing in seconds, default is 1
  114. """
  115. def __init__(self, iterable,
  116. src_dst_cb,
  117. config_cb=config_cb_default,
  118. node_timeout=10,
  119. update_interval=1,
  120. additional_vertexprops=[],
  121. additional_edgeprops=[]):
  122. # given params
  123. self._iterable = iterable
  124. self._src_dst_cb = src_dst_cb
  125. self._config_cb = config_cb
  126. self._node_timeout = node_timeout
  127. # self._update_interval = update_interval
  128. # additional fields
  129. self._graphics_start_thread = threading.Thread(target=self._start_graphics)
  130. self._packet_update_thread = threading.Thread(target=self._packet_read_loop)
  131. self._packet_update_sema = threading.Semaphore(value=0)
  132. # removing vertices/edges in parallel makes trouble, synchronize update and graphics thread
  133. self._cleanup_sema = threading.Semaphore(value=0)
  134. self._want_cleanup = False
  135. self._cleanup_vertices = []
  136. # temporarily paused
  137. self._is_paused = True
  138. self._is_stopped = True
  139. # is visualizer definitely terminated?
  140. self._is_terminated = False
  141. #
  142. self._last_cleanup = time.time()
  143. # self._last_graphic_update = self._last_cleanup
  144. # self._psocket = None
  145. # dict: unique name (src) -> vertex object
  146. self._vertices_dict = {}
  147. # dict: unique name (src_dst) -> edge object
  148. self._edges_dict = {}
  149. # name : object
  150. self._vertex_properties = {}
  151. # name : default value
  152. self._vertex_properties_defaultvalues = {}
  153. self._vertex_livetime = {}
  154. self._edge_properties = {}
  155. # name : default value
  156. self._edge_properties_defaultvalues = {}
  157. self._init_graphwindow(additional_vertexprops, additional_edgeprops)
  158. # add reset-callback listener called when pressing "t"
  159. # only 1 instance allowed, old one will be removed
  160. _key_listener.clear()
  161. _key_listener.append(self._reset_positions)
  162. # TODO: more default properties
  163. DEFAULT_PROPERTIES_VERTEX = [["text", "string", "NODE!!!"],
  164. ["size", "int", 50],
  165. ["shape", "string", "circle"],
  166. ["color", "vector<float>", [0, 0, 0, 0.0]],
  167. ["fill_color", "vector<float>", [1, 1, 1, 0.0]],
  168. ["halo", "bool", False],
  169. ["halo_color", "vector<float>", [1, 0, 0, 0.4]]
  170. ]
  171. DEFAULT_PROPERTIES_EDGE = [["text", "string", "EDGE!!!"],
  172. ["color", "vector<float>", [0, 0, 0, 1]],
  173. ["dash_style", "vector<float>", []]
  174. ]
  175. def _init_graphwindow(self, additional_vertexprops=[], additional_edgeprops=[]):
  176. self._graph = Graph(prune=True, directed=False)
  177. # load properties
  178. self._positions = self._graph.new_vertex_property("vector<float>")
  179. for prop in Visualizer.DEFAULT_PROPERTIES_VERTEX + additional_vertexprops:
  180. self._add_property(True, prop)
  181. for prop in Visualizer.DEFAULT_PROPERTIES_EDGE + additional_edgeprops:
  182. self._add_property(False, prop)
  183. # pos = fruchterman_reingold_layout(self._graph, pos=self._positions)
  184. pos_layout = sfdp_layout(self._graph, K=10, verbose=True, pos=self._positions)
  185. # pos_layout = sfdp_layout(self._graph)
  186. # pos = radial_tree_layout(self._graph, 0)
  187. # pos_layout = self._positions
  188. self._graphwindow = GraphWindow(
  189. self._graph,
  190. # update_layout=True,
  191. # pos=self._positions,
  192. pos=pos_layout,
  193. # TODO: make this dynamic
  194. geometry=(400, 300),
  195. vertex_font_size=10,
  196. vertex_pen_width=1,
  197. # vertex_text_offset=[0,0],
  198. vprops=self._vertex_properties,
  199. edge_font_size=10,
  200. edge_pen_width=1,
  201. # edge_marker_size=12,
  202. # markers added allthough undirected???
  203. # edge_start_marker="arrow",
  204. # edge_end_marker="arrow",
  205. # edge_text_distance=2,
  206. eprops=self._edge_properties)
  207. # set optimal distance in order to make auto-layout working
  208. # self._graphwindow.graph.layout_K = 40
  209. # minimum 1 vertex on graph (avoid bug in graph-tool which leads to division by zero)
  210. # TODO: remove
  211. logger.debug("adding initial vertices")
  212. self._update_vertices("A", "B")
  213. self._update_vertices("B", "A")
  214. """
  215. self._update_vertices("A", "C")
  216. self._update_vertices("A", "D")
  217. self._update_vertices("D", None)
  218. self._update_vertices("E", None)
  219. self._update_vertices("F", None)
  220. self._update_vertices("G", None)
  221. self._update_vertices("H", None)
  222. self._update_vertices("I", None)
  223. self._update_vertices("J", None)
  224. self._update_vertices("K", None)
  225. self._update_vertices("L", None)
  226. self._update_vertices("M", None)
  227. self._update_vertices("M", "L")
  228. self._update_vertices("M", "F")
  229. self._update_vertices("A", "F")
  230. self._update_vertices("F", "F")
  231. self._update_vertices("K", "L")
  232. self._update_vertices("A", "L")
  233. """
  234. def _add_property(self, for_vertex, property_config):
  235. """
  236. Add a new property to be modified.
  237. for_vertex -- add vertex property if True, else add an edge property
  238. property_config -- property description like as list: ["name", "type_description", default_value]
  239. """
  240. logger.debug("adding property (%s): %r" % ("vertex" if for_vertex else "edge", property_config))
  241. if for_vertex:
  242. property = self._graph.new_vertex_property(property_config[1])
  243. self._vertex_properties[property_config[0]] = property
  244. self._vertex_properties_defaultvalues[property_config[0]] = property_config[2]
  245. else:
  246. property = self._graph.new_edge_property(property_config[1])
  247. self._edge_properties[property_config[0]] = property
  248. self._edge_properties_defaultvalues[property_config[0]] = property_config[2]
  249. def _cleanup_graph(self, current_time):
  250. """
  251. Remove vertices (+ attached edges) which are too old.
  252. """
  253. logger.debug("cleaning up graph")
  254. vertex_remove_local = []
  255. for name, last_update in self._vertex_livetime.items():
  256. if current_time - last_update > self._node_timeout:
  257. vertex_remove_local.append(name)
  258. vertex = self._vertices_dict[name]
  259. logger.debug("vertex to remove: %r" % vertex)
  260. # edges in graph should be removed automatically
  261. self._cleanup_vertices.append(vertex)
  262. self._want_cleanup = True
  263. # wait until graphics thread has removed vertices
  264. logger.debug("waiting until vertices are removed")
  265. self._cleanup_sema.acquire()
  266. for name in vertex_remove_local:
  267. del self._vertex_livetime[name]
  268. del self._vertices_dict[name]
  269. # vertex can be placed as _edges_dict[name][...] or _edges_dict[...][name]
  270. try:
  271. del self._edges_dict[name]
  272. except KeyError:
  273. # name not present
  274. pass
  275. for vertex_a in self._edges_dict:
  276. try:
  277. del self._edges_dict[vertex_a][name]
  278. except KeyError:
  279. # name not present
  280. pass
  281. logger.debug("finished removing local vertices")
  282. def _add_vertex(self):
  283. """
  284. Place a new vertex at a random position.
  285. return -- the newly added vertex
  286. """
  287. # random position in start
  288. # TODO: width/height
  289. random.seed(time.time())
  290. x = random.randint(10, 100)
  291. random.seed(time.time() + 1)
  292. y = random.randint(10, 100)
  293. vertex = self._graph.add_vertex()
  294. self._positions[vertex] = (x, y)
  295. # self._positions[vertex] = (50.0, 50.0)
  296. # TODO: find better place for this
  297. # self._reset_positions()
  298. return vertex
  299. def _reset_positions(self):
  300. """
  301. Put all vertices in a close distance in order to reorder them fast afterwards.
  302. """
  303. # logger.debug("resetting")
  304. cnt = 1
  305. for name, vertex in self._vertices_dict.items():
  306. random.seed(cnt + time.time())
  307. x = random.randint(1, 10)
  308. random.seed(cnt + time.time() + 1)
  309. y = random.randint(1, 10)
  310. self._positions[vertex] = (x, y)
  311. cnt += 1
  312. def _update_vertices(self, src, dst=None):
  313. """
  314. Add new vertex identified by src (and dst + edge between them) if not allready present.
  315. src -- unique source string
  316. dst -- unique destination string or None
  317. return -- vertex_source, vertex_dest, edge where vertex_dest and edge can be None
  318. """
  319. vertex_to_update = []
  320. # create new vertex
  321. try:
  322. vertex_src = self._vertices_dict[src]
  323. except KeyError:
  324. vertex_src = self._add_vertex()
  325. self._vertices_dict[src] = vertex_src
  326. vertex_to_update.append(vertex_src)
  327. vertex_dst = None
  328. edge_src_dst = None
  329. if dst is not None:
  330. # initiate dst and add edge between src<->dst
  331. try:
  332. vertex_dst = self._vertices_dict[dst]
  333. except KeyError:
  334. vertex_dst = self._add_vertex()
  335. self._vertices_dict[dst] = vertex_dst
  336. vertex_to_update.append(vertex_dst)
  337. edge = sorted([src, dst])
  338. add_edge = False
  339. try:
  340. edge_src_dst = self._edges_dict[edge[0]][edge[1]]
  341. except KeyError:
  342. if not edge[0] in self._edges_dict:
  343. self._edges_dict[edge[0]] = {}
  344. add_edge = True
  345. if add_edge:
  346. # TODO: don't add second edge but update arrows (both directions)
  347. edge_src_dst = self._graph.add_edge(vertex_src, vertex_dst)
  348. self._edges_dict[edge[0]][edge[1]] = edge_src_dst
  349. # logger.debug("!!!!! adding edge")
  350. # set default property values for edge
  351. for k, v in self._edge_properties_defaultvalues.items():
  352. # logger.debug("edge default val: %r: %s=%s" % (self._edge_properties[k], k, v))
  353. self._edge_properties[k][edge_src_dst] = v
  354. for vertex in vertex_to_update:
  355. # set default property values for vertices
  356. for k, v in self._vertex_properties_defaultvalues.items():
  357. # logger.debug("vertex default val: %r: %s=%s" % (self._vertex_properties[k], k, v))
  358. self._vertex_properties[k][vertex] = v
  359. return vertex_src, vertex_dst, edge_src_dst
  360. def _packet_read_loop(self):
  361. """
  362. Read packets from _iterable and update graph data until StopIteration
  363. is thrown by it or Visualizer is stopped.
  364. Take packets (pkt) instead of eg raw bytes for _src_dst_cb and _config_cb:
  365. avoid unneeded reparsing.
  366. """
  367. for pkt in self._iterable:
  368. # time.sleep(1)
  369. if self._is_paused:
  370. self._packet_update_sema.acquire()
  371. if self._is_stopped:
  372. break
  373. # analyze packet and update graph
  374. src, dst = self._src_dst_cb(pkt)
  375. if src is None:
  376. continue
  377. vertex_src, vertex_dst, edge = self._update_vertices(src, dst)
  378. self._config_cb(pkt,
  379. vertex_src,
  380. vertex_dst,
  381. edge,
  382. self._vertex_properties,
  383. self._edge_properties)
  384. # cleanup logic
  385. current_time = time.time()
  386. if dst is not None:
  387. self._vertex_livetime[dst] = current_time
  388. self._vertex_livetime[src] = current_time
  389. if current_time - self._last_cleanup > self._node_timeout:
  390. # TODO: temporarily disabled
  391. # self._cleanup_graph(current_time)
  392. self._last_cleanup = current_time
  393. logger.debug("finished iterating packets")
  394. def _update_graphics(self):
  395. if self._want_cleanup:
  396. for vertex in self._cleanup_vertices:
  397. logger.debug("removing vertex: %r" % vertex)
  398. # remove in/out-edges
  399. self._graph.clear_vertex(vertex)
  400. # nomen est omen
  401. self._graph.remove_vertex(vertex)
  402. self._cleanup_vertices.clear()
  403. self._want_cleanup = False
  404. self._cleanup_sema.release()
  405. # self._graphwindow.graph.regenerate_surface(lazy=True)
  406. self._graphwindow.graph.regenerate_surface(lazy=False)
  407. self._graphwindow.graph.queue_draw()
  408. return True
  409. def _start_graphics(self):
  410. logger.debug("initiating graphics")
  411. # cid = GObject.idle_add(self._update_graphics)
  412. cid = GObject.timeout_add(150, self._update_graphics)
  413. self._graphwindow.connect("delete_event", Gtk.main_quit)
  414. self._graphwindow.show_all()
  415. Gtk.main()
  416. logger.debug("window was closed...stopping")
  417. # no graphics = window was closed = nothing to be done anymore
  418. self.stop()
  419. def start(self):
  420. if self._is_terminated:
  421. return
  422. logger.debug("starting visualizer")
  423. self._is_stopped = False
  424. self._is_paused = False
  425. self._graphics_start_thread.start()
  426. self._packet_update_thread.start()
  427. def pause(self):
  428. if self._is_stopped:
  429. return
  430. logger.debug("pausing visualizer")
  431. self._is_paused = True
  432. def resume(self):
  433. if self._is_stopped:
  434. return
  435. logger.debug("resuming visualizer")
  436. self._is_paused = False
  437. # TODO: check locking mechanisms
  438. self._packet_update_sema.release()
  439. def stop(self):
  440. if self._is_terminated:
  441. return
  442. logger.debug("stopping visualizer")
  443. self._is_terminated = True
  444. # unlock locked packet-reader
  445. self.resume()
  446. self._is_stopped = True