Merge branch 'master' into 1.1
[tinc] / gui / tinc-gui
1 #!/usr/bin/python
2
3 # tinc-gui -- GUI for controlling a running tincd
4 # Copyright (C) 2009-2012 Guus Sliepen <guus@tinc-vpn.org>
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License along
17 # with this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
19
20 import string
21 import socket
22 import wx
23 import sys
24 import os
25 import platform
26 import time
27 from wx.lib.mixins.listctrl import ColumnSorterMixin
28 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
29
30 if platform.system == 'Windows':
31         import _winreg
32
33 # Classes to interface with a running tinc daemon
34
35 REQ_STOP = 0
36 REQ_RELOAD = 1
37 REQ_RESTART = 2
38 REQ_DUMP_NODES = 3
39 REQ_DUMP_EDGES = 4
40 REQ_DUMP_SUBNETS = 5
41 REQ_DUMP_CONNECTIONS = 6
42 REQ_DUMP_GRAPH = 7
43 REQ_PURGE = 8
44 REQ_SET_DEBUG = 9
45 REQ_RETRY = 10
46 REQ_CONNECT = 11
47 REQ_DISCONNECT = 12
48
49 ID = 0
50 ACK = 4
51 CONTROL = 18
52
53 class Node:
54         def parse(self, args):
55                 self.name = args[0]
56                 self.address = args[1]
57                 self.port = args[3]
58                 self.cipher = int(args[4])
59                 self.digest = int(args[5])
60                 self.maclength = int(args[6])
61                 self.compression = int(args[7])
62                 self.options = int(args[8], 0x10)
63                 self.status = int(args[9], 0x10)
64                 self.nexthop = args[10]
65                 self.via = args[11]
66                 self.distance = int(args[12])
67                 self.pmtu = int(args[13])
68                 self.minmtu = int(args[14])
69                 self.maxmtu = int(args[15])
70                 self.last_state_change = float(args[16])
71
72                 self.subnets = {}
73
74 class Edge:
75         def parse(self, args):
76                 self.fr = args[0]
77                 self.to = args[1]
78                 self.address = args[2]
79                 self.port = args[4]
80                 self.options = int(args[5], 16)
81                 self.weight = int(args[6])
82
83 class Subnet:
84         def parse(self, args):
85                 if args[0].find('#') >= 0:
86                         (address, self.weight) = args[0].split('#', 1)
87                 else:
88                         self.weight = 10
89                         address = args[0]
90
91                 if address.find('/') >= 0:
92                         (self.address, self.prefixlen) = address.split('/', 1)
93                 else:
94                         self.address = address
95                         self.prefixlen = '48'
96
97                 self.owner = args[1]    
98
99 class Connection:
100         def parse(self, args):
101                 self.name = args[0]
102                 self.address = args[1]
103                 self.port = args[3]
104                 self.options = int(args[4], 0x10)
105                 self.socket = int(args[5])
106                 self.status = int(args[6], 0x10)
107                 self.weight = 123
108
109 class VPN:
110         confdir = '/etc/tinc'
111         piddir = '/var/run/'
112
113         def connect(self):
114                 f = open(self.pidfile)
115                 info = string.split(f.readline())
116                 f.close()
117                 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
118                 s.connect((info[2], int(info[4])))
119                 self.sf = s.makefile()
120                 s.close()
121                 hello = string.split(self.sf.readline())
122                 self.name = hello[1]
123                 self.sf.write('0 ^' + info[1] + ' 17\r\n')
124                 self.sf.flush()
125                 resp = string.split(self.sf.readline())
126                 self.port = info[4]
127                 self.nodes = {}
128                 self.edges = {}
129                 self.subnets = {}
130                 self.connections = {}
131                 self.refresh()
132
133         def refresh(self):
134                 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
135                 self.sf.flush()
136
137                 for node in self.nodes.values():
138                         node.visited = False
139                 for edge in self.edges.values():
140                         edge.visited = False
141                 for subnet in self.subnets.values():
142                         subnet.visited = False
143                 for connections in self.connections.values():
144                         connections.visited = False
145
146                 while True:
147                         resp = string.split(self.sf.readline())
148                         if len(resp) < 2:
149                                 break
150                         if resp[0] != '18':
151                                 break
152                         if resp[1] == '3':
153                                 if len(resp) < 19:
154                                         continue
155                                 node = self.nodes.get(resp[2]) or Node()
156                                 node.parse(resp[2:])
157                                 node.visited = True
158                                 self.nodes[resp[2]] = node
159                         elif resp[1] == '4':
160                                 if len(resp) < 9:
161                                         continue
162                                 edge = self.nodes.get((resp[2], resp[3])) or Edge()
163                                 edge.parse(resp[2:])
164                                 edge.visited = True
165                                 self.edges[(resp[2], resp[3])] = edge
166                         elif resp[1] == '5':
167                                 if len(resp) < 4:
168                                         continue
169                                 subnet = self.subnets.get((resp[2], resp[3])) or Subnet()
170                                 subnet.parse(resp[2:])
171                                 subnet.visited = True
172                                 self.subnets[(resp[2], resp[3])] = subnet
173                                 self.nodes[subnet.owner].subnets[resp[2]] = subnet
174                         elif resp[1] == '6':
175                                 if len(resp) < 9:
176                                         break
177                                 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection()
178                                 connection.parse(resp[2:])
179                                 connection.visited = True
180                                 self.connections[(resp[2], resp[3], resp[5])] = connection
181                         else:
182                                 break
183
184                 for key, subnet in self.subnets.items():
185                         if not subnet.visited:
186                                 del self.subnets[key]
187
188                 for key, edge in self.edges.items():
189                         if not edge.visited:
190                                 del self.edges[key]
191
192                 for key, node in self.nodes.items():
193                         if not node.visited:
194                                 del self.nodes[key]
195                         else:
196                                 for key, subnet in node.subnets.items():
197                                         if not subnet.visited:
198                                                 del node.subnets[key]
199
200                 for key, connection in self.connections.items():
201                         if not connection.visited:
202                                 del self.connections[key]
203
204         def close(self):
205                 self.sf.close()
206
207         def disconnect(self, name):
208                 self.sf.write('18 12 ' + name + '\r\n')
209                 self.sf.flush()
210                 resp = string.split(self.sf.readline())
211
212         def debug(self, level = -1):
213                 self.sf.write('18 9 ' + str(level) + '\r\n')
214                 self.sf.flush()
215                 resp = string.split(self.sf.readline())
216                 return int(resp[2])
217
218         def __init__(self, netname = None, pidfile = None):
219                 if platform.system == 'Windows':
220                         try:
221                                 reg = _winreg.ConnectRegistry(None, HKEY_LOCAL_MACHINE)
222                                 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc")
223                                 VPN.confdir = _winreg.QueryValue(key, None)
224                         except WindowsError:
225                                 pass
226
227                 if netname:
228                         self.netname = netname
229                         self.confbase = os.path.join(VPN.confdir, netname)
230                 else:
231                         self.confbase = VPN.confdir
232
233                 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
234
235                 if pidfile != None:
236                         self.pidfile = pidfile
237                 else:
238                         if platform.system == 'Windows':
239                                 self.pidfile = os.path.join(self.confbase, 'pid')
240                         else:
241                                 if netname:
242                                         self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
243                                 else:
244                                         self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
245
246 # GUI starts here
247
248 argv0 = sys.argv[0]
249 del sys.argv[0]
250 netname = None
251 pidfile = None
252
253 def usage(exitcode = 0):
254         print('Usage: ' + argv0 + ' [options]')
255         print('\nValid options are:')
256         print('  -n, --net=NETNAME       Connect to net NETNAME.')
257         print('      --pidfile=FILENAME  Read control cookie from FILENAME.')
258         print('      --help              Display this help and exit.')
259         print('\nReport bugs to tinc@tinc-vpn.org.')
260         sys.exit(exitcode)
261
262 while sys.argv:
263         if sys.argv[0] in ('-n', '--net'):
264                 del sys.argv[0]
265                 netname = sys.argv[0]
266         elif sys.argv[0] in ('--pidfile'):
267                 del sys.argv[0]
268                 pidfile = sys.argv[0]
269         elif sys.argv[0] in ('--help'):
270                 usage(0)
271         else:
272                 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
273                 usage(1)
274
275         del sys.argv[0]
276
277 if netname == None:
278         netname = os.getenv("NETNAME")
279
280 if netname == ".":
281         netname = None
282
283 vpn = VPN(netname, pidfile)
284 vpn.connect()
285
286 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
287     def __init__(self, parent, style):
288         wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
289         ListCtrlAutoWidthMixin.__init__(self)
290         ColumnSorterMixin.__init__(self, 16)
291
292     def GetListCtrl(self):
293         return self
294
295
296 class SettingsPage(wx.Panel):
297         def OnDebugLevel(self, event):
298                 vpn.debug(self.debug.GetValue())
299
300         def __init__(self, parent, id):
301                 wx.Panel.__init__(self, parent, id)
302                 grid = wx.FlexGridSizer(cols = 2)
303                 grid.AddGrowableCol(1, 1)
304
305                 namelabel = wx.StaticText(self, -1, 'Name:')
306                 self.name = wx.TextCtrl(self, -1, vpn.name)
307                 grid.Add(namelabel)
308                 grid.Add(self.name, 1, wx.EXPAND)
309
310                 portlabel = wx.StaticText(self, -1, 'Port:')
311                 self.port = wx.TextCtrl(self, -1, vpn.port)
312                 grid.Add(portlabel)
313                 grid.Add(self.port)
314
315                 debuglabel = wx.StaticText(self, -1, 'Debug level:')
316                 self.debug = wx.SpinCtrl(self, min = 0, max = 5, initial = vpn.debug())
317                 self.debug.Bind(wx.EVT_SPINCTRL, self.OnDebugLevel)
318                 grid.Add(debuglabel)
319                 grid.Add(self.debug)
320
321                 modelabel = wx.StaticText(self, -1, 'Mode:')
322                 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
323                 grid.Add(modelabel)
324                 grid.Add(self.mode)
325
326                 self.SetSizer(grid)
327
328 class ConnectionsPage(wx.Panel):
329         def __init__(self, parent, id):
330                 wx.Panel.__init__(self, parent, id)
331                 self.list = SuperListCtrl(self, id)
332                 self.list.InsertColumn(0, 'Name')
333                 self.list.InsertColumn(1, 'Address')
334                 self.list.InsertColumn(2, 'Port')
335                 self.list.InsertColumn(3, 'Options')
336                 self.list.InsertColumn(4, 'Weight')
337
338                 hbox = wx.BoxSizer(wx.HORIZONTAL)
339                 hbox.Add(self.list, 1, wx.EXPAND)
340                 self.SetSizer(hbox)
341                 self.refresh()
342
343         class ContextMenu(wx.Menu):
344                 def __init__(self, item):
345                         wx.Menu.__init__(self)
346
347                         self.item = item
348
349                         disconnect = wx.MenuItem(self, -1, 'Disconnect')
350                         self.AppendItem(disconnect)
351                         self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
352
353                 def OnDisconnect(self, event):
354                         vpn.disconnect(self.item[0])
355
356         def OnContext(self, event):
357                 i = event.GetIndex()
358                 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
359
360         def refresh(self):
361                 sortstate = self.list.GetSortState()
362                 self.list.itemDataMap = {}
363                 i = 0
364
365                 for key, connection in vpn.connections.items():
366                         if self.list.GetItemCount() <= i:
367                                 self.list.InsertStringItem(i, connection.name)
368                         else:
369                                 self.list.SetStringItem(i, 0, connection.name)
370                         self.list.SetStringItem(i, 1, connection.address)
371                         self.list.SetStringItem(i, 2, connection.port)
372                         self.list.SetStringItem(i, 3, str(connection.options))
373                         self.list.SetStringItem(i, 4, str(connection.weight))
374                         self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options, connection.weight)
375                         self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnContext)
376                         self.list.SetItemData(i, i)
377                         i += 1
378
379                 while self.list.GetItemCount() > i:
380                         self.list.DeleteItem(self.list.GetItemCount() - 1)
381
382                 self.list.SortListItems(sortstate[0], sortstate[1])
383
384 class NodesPage(wx.Panel):
385         def __init__(self, parent, id):
386                 wx.Panel.__init__(self, parent, id)
387                 self.list = SuperListCtrl(self, id)
388                 self.list.InsertColumn( 0, 'Name')
389                 self.list.InsertColumn( 1, 'Address')
390                 self.list.InsertColumn( 2, 'Port')
391                 self.list.InsertColumn( 3, 'Cipher')
392                 self.list.InsertColumn( 4, 'Digest')
393                 self.list.InsertColumn( 5, 'MACLength')
394                 self.list.InsertColumn( 6, 'Compression')
395                 self.list.InsertColumn( 7, 'Options')
396                 self.list.InsertColumn( 8, 'Status')
397                 self.list.InsertColumn( 9, 'Nexthop')
398                 self.list.InsertColumn(10, 'Via')
399                 self.list.InsertColumn(11, 'Distance')
400                 self.list.InsertColumn(12, 'PMTU')
401                 self.list.InsertColumn(13, 'Min MTU')
402                 self.list.InsertColumn(14, 'Max MTU')
403                 self.list.InsertColumn(15, 'Since')
404
405                 hbox = wx.BoxSizer(wx.HORIZONTAL)
406                 hbox.Add(self.list, 1, wx.EXPAND)
407                 self.SetSizer(hbox)
408                 self.refresh()
409
410         def refresh(self):
411                 sortstate = self.list.GetSortState()
412                 self.list.itemDataMap = {}
413                 i = 0
414
415                 for key, node in vpn.nodes.items():
416                         if self.list.GetItemCount() <= i:
417                                 self.list.InsertStringItem(i, node.name)
418                         else:
419                                 self.list.SetStringItem(i,  0, node.name)
420                         self.list.SetStringItem(i,  1, node.address)
421                         self.list.SetStringItem(i,  2, node.port)
422                         self.list.SetStringItem(i,  3, str(node.cipher))
423                         self.list.SetStringItem(i,  4, str(node.digest))
424                         self.list.SetStringItem(i,  5, str(node.maclength))
425                         self.list.SetStringItem(i,  6, str(node.compression))
426                         self.list.SetStringItem(i,  7, format(node.options, "x"))
427                         self.list.SetStringItem(i,  8, format(node.status, "04x"))
428                         self.list.SetStringItem(i,  9, node.nexthop)
429                         self.list.SetStringItem(i, 10, node.via)
430                         self.list.SetStringItem(i, 11, str(node.distance))
431                         self.list.SetStringItem(i, 12, str(node.pmtu))
432                         self.list.SetStringItem(i, 13, str(node.minmtu))
433                         self.list.SetStringItem(i, 14, str(node.maxmtu))
434                         if node.last_state_change:
435                                 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
436                         else:
437                                 since = "never"
438                         self.list.SetStringItem(i, 15, since)
439                         self.list.itemDataMap[i] = (node.name, node.address, node.port, node.cipher, node.digest, node.maclength, node.compression, node.options, node.status, node.nexthop, node.via, node.distance, node.pmtu, node.minmtu, node.maxmtu, since)
440                         self.list.SetItemData(i, i)
441                         i += 1
442
443                 while self.list.GetItemCount() > i:
444                         self.list.DeleteItem(self.list.GetItemCount() - 1)
445
446                 self.list.SortListItems(sortstate[0], sortstate[1])
447
448 class EdgesPage(wx.Panel):
449         def __init__(self, parent, id):
450                 wx.Panel.__init__(self, parent, id)
451                 self.list = SuperListCtrl(self, id)
452                 self.list.InsertColumn(0, 'From')
453                 self.list.InsertColumn(1, 'To')
454                 self.list.InsertColumn(2, 'Address')
455                 self.list.InsertColumn(3, 'Port')
456                 self.list.InsertColumn(4, 'Options')
457                 self.list.InsertColumn(5, 'Weight')
458
459                 hbox = wx.BoxSizer(wx.HORIZONTAL)
460                 hbox.Add(self.list, 1, wx.EXPAND)
461                 self.SetSizer(hbox)
462                 self.refresh()
463
464         def refresh(self):
465                 sortstate = self.list.GetSortState()
466                 self.list.itemDataMap = {}
467                 i = 0
468
469                 for key, edge in vpn.edges.items():
470                         if self.list.GetItemCount() <= i:
471                                 self.list.InsertStringItem(i, edge.fr)
472                         else:
473                                 self.list.SetStringItem(i, 0, edge.fr)
474                         self.list.SetStringItem(i, 1, edge.to)
475                         self.list.SetStringItem(i, 2, edge.address)
476                         self.list.SetStringItem(i, 3, edge.port)
477                         self.list.SetStringItem(i, 4, format(edge.options, "x"))
478                         self.list.SetStringItem(i, 5, str(edge.weight))
479                         self.list.itemDataMap[i] = (edge.fr, edge.to, edge.address, edge.port, edge.options, edge.weight)
480                         self.list.SetItemData(i, i)
481                         i += 1
482
483                 while self.list.GetItemCount() > i:
484                         self.list.DeleteItem(self.list.GetItemCount() - 1)
485
486                 self.list.SortListItems(sortstate[0], sortstate[1])
487
488 class SubnetsPage(wx.Panel):
489         def __init__(self, parent, id):
490                 wx.Panel.__init__(self, parent, id)
491                 self.list = SuperListCtrl(self, id)
492                 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
493                 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
494                 self.list.InsertColumn(2, 'Owner')
495                 hbox = wx.BoxSizer(wx.HORIZONTAL)
496                 hbox.Add(self.list, 1, wx.EXPAND)
497                 self.SetSizer(hbox)
498                 self.refresh()
499
500         def refresh(self):
501                 sortstate = self.list.GetSortState()
502                 self.list.itemDataMap = {}
503                 i = 0
504
505                 for key, subnet in vpn.subnets.items():
506                         if self.list.GetItemCount() <= i:
507                                 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
508                         else:
509                                 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
510                         self.list.SetStringItem(i, 1, subnet.weight)
511                         self.list.SetStringItem(i, 2, subnet.owner)
512                         self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
513                         self.list.SetItemData(i, i)
514                         i += 1
515
516                 while self.list.GetItemCount() > i:
517                         self.list.DeleteItem(self.list.GetItemCount() - 1)
518
519                 self.list.SortListItems(sortstate[0], sortstate[1])
520
521 class StatusPage(wx.Panel):
522         def __init__(self, parent, id):
523                 wx.Panel.__init__(self, parent, id)
524
525 class GraphPage(wx.Window):
526         def __init__(self, parent, id):
527                 wx.Window.__init__(self, parent, id)
528
529 class NetPage(wx.Notebook):
530         def __init__(self, parent, id):
531                 wx.Notebook.__init__(self, parent)
532                 self.settings = SettingsPage(self, id)
533                 self.connections = ConnectionsPage(self, id)
534                 self.nodes = NodesPage(self, id)
535                 self.edges = EdgesPage(self, id)
536                 self.subnets = SubnetsPage(self, id)
537                 self.graph = GraphPage(self, id)
538                 self.status = StatusPage(self, id)
539
540                 self.AddPage(self.settings, 'Settings')
541                 #self.AddPage(self.status, 'Status')
542                 self.AddPage(self.connections, 'Connections')
543                 self.AddPage(self.nodes, 'Nodes')
544                 self.AddPage(self.edges, 'Edges')
545                 self.AddPage(self.subnets, 'Subnets')
546                 #self.AddPage(self.graph, 'Graph')
547                 
548
549 class MainWindow(wx.Frame):
550         def OnQuit(self, event):
551                 app.ExitMainLoop()
552
553         def OnTimer(self, event):
554                 vpn.refresh()
555                 self.np.nodes.refresh()
556                 self.np.subnets.refresh()
557                 self.np.edges.refresh()
558                 self.np.connections.refresh()
559
560         def __init__(self, parent, id, title):
561                 wx.Frame.__init__(self, parent, id, title)
562
563                 menubar = wx.MenuBar()
564                 file = wx.Menu()
565                 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
566                 menubar.Append(file, '&File')
567
568                 #nb = wx.Notebook(self, -1)
569                 #nb.SetPadding((0, 0))
570                 self.np = NetPage(self, -1)
571                 #nb.AddPage(np, 'VPN')
572                 
573                 self.timer = wx.Timer(self, -1)
574                 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
575                 self.timer.Start(1000)
576                 self.Bind(wx.EVT_MENU, self.OnQuit, id=1)
577                 self.SetMenuBar(menubar)
578                 self.Show()
579
580 app = wx.App()
581 mw = MainWindow(None, -1, 'Tinc GUI')
582
583 #def OnTaskBarIcon(event):
584 #       mw.Raise()
585 #
586 #icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
587 #taskbaricon = wx.TaskBarIcon()
588 #taskbaricon.SetIcon(icon, 'Tinc GUI')
589 #wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)
590
591 app.MainLoop()
592 vpn.close()