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