tinc-gui: Properly initialize class attributes for VPN in __init__
[tinc] / gui / tinc-gui
1 #!/usr/bin/env python2
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 os
24 import platform
25 import time
26 from argparse import ArgumentParser
27
28 import wx
29 from wx.lib.mixins.listctrl import ColumnSorterMixin
30 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
31
32 if platform.system() == 'Windows':
33     import _winreg
34
35 # Classes to interface with a running tinc daemon
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 = 'n/a'
124
125
126 class VPN(object):
127     def __init__(self, netname=None, pidfile=None, confdir='/etc/tinc', piddir='/run'):
128         if platform.system() == 'Windows':
129             sam = _winreg.KEY_READ
130             if platform.machine().endswith('64'):
131                 sam = sam | _winreg.KEY_WOW64_64KEY
132             try:
133                 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
134                 try:
135                     key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
136                 except WindowsError:
137                     key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
138                 confdir = _winreg.QueryValue(key, None)
139             except WindowsError:
140                 pass
141
142         if netname:
143             self.netname = netname
144             self.confbase = os.path.join(confdir, netname)
145         else:
146             self.confbase = confdir
147
148         self.tincconf = os.path.join(self.confbase, 'tinc.conf')
149
150         if pidfile is not None:
151             self.pidfile = pidfile
152         else:
153             if platform.system() == 'Windows':
154                 self.pidfile = os.path.join(self.confbase, 'pid')
155             else:
156                 if netname:
157                     self.pidfile = os.path.join(piddir, 'tinc.' + netname + '.pid')
158                 else:
159                     self.pidfile = os.path.join(piddir, 'tinc.pid')
160
161         self.sf = None
162         self.name = None
163         self.port = None
164         self.nodes = {}
165         self.edges = {}
166         self.subnets = {}
167         self.connections = {}
168
169     def connect(self):
170         # read the pidfile
171         f = open(self.pidfile)
172         info = string.split(f.readline())
173         f.close()
174
175         # check if there is a UNIX socket as well
176         if self.pidfile.endswith('.pid'):
177             unixfile = self.pidfile.replace('.pid', '.socket');
178         else:
179             unixfile = self.pidfile + '.socket';
180
181         if os.path.exists(unixfile):
182             # use it if it exists
183             print(unixfile + " exists!");
184             s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
185             s.connect(unixfile)
186         else:
187             # otherwise connect via TCP
188             print(unixfile + " does not exist.");
189             if ':' in info[2]:
190                 af = socket.AF_INET6
191             else:
192                 af = socket.AF_INET
193             s = socket.socket(af, socket.SOCK_STREAM)
194             s.connect((info[2], int(info[4])))
195
196         self.sf = s.makefile()
197         s.close()
198         hello = string.split(self.sf.readline())
199         self.name = hello[1]
200         self.sf.write('0 ^' + info[1] + ' 17\r\n')
201         self.sf.flush()
202         resp = string.split(self.sf.readline())
203         self.port = info[4]
204         self.refresh()
205
206     def refresh(self):
207         for request in (REQ_DUMP_NODES, REQ_DUMP_EDGES, REQ_DUMP_SUBNETS, REQ_DUMP_CONNECTIONS):
208             self.sf.write('{} {}\r\n'.format(CONTROL, request))
209         self.sf.flush()
210
211         for node in self.nodes.values():
212             node.visited = False
213         for edge in self.edges.values():
214             edge.visited = False
215         for subnet in self.subnets.values():
216             subnet.visited = False
217         for connections in self.connections.values():
218             connections.visited = False
219
220         while True:
221             resp = string.split(self.sf.readline())
222             if len(resp) < 2:
223                 break
224             if resp[0] != '18':
225                 break
226             if resp[1] == '3':
227                 if len(resp) < 19:
228                     continue
229                 node = self.nodes.get(resp[2]) or Node(resp[2:])
230                 node.visited = True
231                 self.nodes[resp[2]] = node
232             elif resp[1] == '4':
233                 if len(resp) < 9:
234                     continue
235                 edge = self.nodes.get((resp[2], resp[3])) or Edge(resp[2:])
236                 edge.visited = True
237                 self.edges[(resp[2], resp[3])] = edge
238             elif resp[1] == '5':
239                 if len(resp) < 4:
240                     continue
241                 subnet = self.subnets.get((resp[2], resp[3])) or Subnet(resp[2:])
242                 subnet.visited = True
243                 self.subnets[(resp[2], resp[3])] = subnet
244                 if subnet.owner == "(broadcast)":
245                     continue
246                 self.nodes[subnet.owner].subnets[resp[2]] = subnet
247             elif resp[1] == '6':
248                 if len(resp) < 9:
249                     break
250                 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection(resp[2:])
251                 connection.visited = True
252                 self.connections[(resp[2], resp[3], resp[5])] = connection
253             else:
254                 break
255
256         for key, subnet in self.subnets.items():
257             if not subnet.visited:
258                 del self.subnets[key]
259
260         for key, edge in self.edges.items():
261             if not edge.visited:
262                 del self.edges[key]
263
264         for key, node in self.nodes.items():
265             if not node.visited:
266                 del self.nodes[key]
267             else:
268                 for key, subnet in node.subnets.items():
269                     if not subnet.visited:
270                         del node.subnets[key]
271
272         for key, connection in self.connections.items():
273             if not connection.visited:
274                 del self.connections[key]
275
276     def close(self):
277         self.sf.close()
278
279     def disconnect(self, name):
280         self.sf.write('18 12 ' + name + '\r\n')
281         self.sf.flush()
282         resp = string.split(self.sf.readline())
283
284     def debug(self, level=-1):
285         self.sf.write('18 9 ' + str(level) + '\r\n')
286         self.sf.flush()
287         resp = string.split(self.sf.readline())
288         return int(resp[2])
289
290
291 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
292     def __init__(self, parent, style):
293         wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
294         ListCtrlAutoWidthMixin.__init__(self)
295         ColumnSorterMixin.__init__(self, 16)
296
297     def GetListCtrl(self):
298         return self
299
300
301 class SettingsPage(wx.Panel):
302     def on_debug_level(self, event):
303         vpn.debug(self.debug.GetValue())
304
305     def __init__(self, parent, id):
306         wx.Panel.__init__(self, parent, id)
307         grid = wx.FlexGridSizer(cols=2)
308         grid.AddGrowableCol(1, 1)
309
310         namelabel = wx.StaticText(self, -1, 'Name:')
311         self.name = wx.TextCtrl(self, -1, vpn.name)
312         grid.Add(namelabel)
313         grid.Add(self.name, 1, wx.EXPAND)
314
315         portlabel = wx.StaticText(self, -1, 'Port:')
316         self.port = wx.TextCtrl(self, -1, vpn.port)
317         grid.Add(portlabel)
318         grid.Add(self.port)
319
320         debuglabel = wx.StaticText(self, -1, 'Debug level:')
321         self.debug = wx.SpinCtrl(self, min=0, max=5, initial=vpn.debug())
322         self.debug.Bind(wx.EVT_SPINCTRL, self.on_debug_level)
323         grid.Add(debuglabel)
324         grid.Add(self.debug)
325
326         modelabel = wx.StaticText(self, -1, 'Mode:')
327         self.mode = wx.ComboBox(self, -1, style=wx.CB_READONLY, value='Router', choices=['Router', 'Switch', 'Hub'])
328         grid.Add(modelabel)
329         grid.Add(self.mode)
330
331         self.SetSizer(grid)
332
333
334 class ConnectionsPage(wx.Panel):
335     def __init__(self, parent, id):
336         wx.Panel.__init__(self, parent, id)
337         self.list = SuperListCtrl(self, id)
338         self.list.InsertColumn(0, 'Name')
339         self.list.InsertColumn(1, 'Address')
340         self.list.InsertColumn(2, 'Port')
341         self.list.InsertColumn(3, 'Options')
342         self.list.InsertColumn(4, 'Weight')
343
344         hbox = wx.BoxSizer(wx.HORIZONTAL)
345         hbox.Add(self.list, 1, wx.EXPAND)
346         self.SetSizer(hbox)
347         self.refresh()
348
349     class ContextMenu(wx.Menu):
350         def __init__(self, item):
351             wx.Menu.__init__(self)
352
353             self.item = item
354
355             disconnect = wx.MenuItem(self, -1, 'Disconnect')
356             self.AppendItem(disconnect)
357             self.Bind(wx.EVT_MENU, self.on_disconnect, id=disconnect.GetId())
358
359         def on_disconnect(self, event):
360             vpn.disconnect(self.item[0])
361
362     def on_context(self, event):
363         idx = event.GetIndex()
364         self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
365
366     def refresh(self):
367         sortstate = self.list.GetSortState()
368         self.list.itemDataMap = {}
369         i = 0
370
371         for key, connection in vpn.connections.items():
372             if self.list.GetItemCount() <= i:
373                 self.list.InsertStringItem(i, connection.name)
374             else:
375                 self.list.SetStringItem(i, 0, connection.name)
376             self.list.SetStringItem(i, 1, connection.address)
377             self.list.SetStringItem(i, 2, connection.port)
378             self.list.SetStringItem(i, 3, str(connection.options))
379             self.list.SetStringItem(i, 4, str(connection.weight))
380             self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options,
381                                         connection.weight)
382             self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_context)
383             self.list.SetItemData(i, i)
384             i += 1
385
386         while self.list.GetItemCount() > i:
387             self.list.DeleteItem(self.list.GetItemCount() - 1)
388
389         self.list.SortListItems(sortstate[0], sortstate[1])
390
391
392 class NodesPage(wx.Panel):
393     def __init__(self, parent, id):
394         wx.Panel.__init__(self, parent, id)
395         self.list = SuperListCtrl(self, id)
396         self.list.InsertColumn(0, 'Name')
397         self.list.InsertColumn(1, 'Address')
398         self.list.InsertColumn(2, 'Port')
399         self.list.InsertColumn(3, 'Cipher')
400         self.list.InsertColumn(4, 'Digest')
401         self.list.InsertColumn(5, 'MACLength')
402         self.list.InsertColumn(6, 'Compression')
403         self.list.InsertColumn(7, 'Options')
404         self.list.InsertColumn(8, 'Status')
405         self.list.InsertColumn(9, 'Nexthop')
406         self.list.InsertColumn(10, 'Via')
407         self.list.InsertColumn(11, 'Distance')
408         self.list.InsertColumn(12, 'PMTU')
409         self.list.InsertColumn(13, 'Min MTU')
410         self.list.InsertColumn(14, 'Max MTU')
411         self.list.InsertColumn(15, 'Since')
412
413         hbox = wx.BoxSizer(wx.HORIZONTAL)
414         hbox.Add(self.list, 1, wx.EXPAND)
415         self.SetSizer(hbox)
416         self.refresh()
417
418     def refresh(self):
419         sortstate = self.list.GetSortState()
420         self.list.itemDataMap = {}
421         i = 0
422
423         for key, node in vpn.nodes.items():
424             if self.list.GetItemCount() <= i:
425                 self.list.InsertStringItem(i, node.name)
426             else:
427                 self.list.SetStringItem(i, 0, node.name)
428             self.list.SetStringItem(i, 1, node.address)
429             self.list.SetStringItem(i, 2, node.port)
430             self.list.SetStringItem(i, 3, str(node.cipher))
431             self.list.SetStringItem(i, 4, str(node.digest))
432             self.list.SetStringItem(i, 5, str(node.maclength))
433             self.list.SetStringItem(i, 6, str(node.compression))
434             self.list.SetStringItem(i, 7, format(node.options, "x"))
435             self.list.SetStringItem(i, 8, format(node.status, "04x"))
436             self.list.SetStringItem(i, 9, node.nexthop)
437             self.list.SetStringItem(i, 10, node.via)
438             self.list.SetStringItem(i, 11, str(node.distance))
439             self.list.SetStringItem(i, 12, str(node.pmtu))
440             self.list.SetStringItem(i, 13, str(node.minmtu))
441             self.list.SetStringItem(i, 14, str(node.maxmtu))
442             if node.last_state_change:
443                 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
444             else:
445                 since = "never"
446             self.list.SetStringItem(i, 15, since)
447             self.list.itemDataMap[i] = (node.name, node.address, node.port, node.cipher, node.digest, node.maclength,
448                                         node.compression, node.options, node.status, node.nexthop, node.via,
449                                         node.distance, node.pmtu, node.minmtu, node.maxmtu, since)
450             self.list.SetItemData(i, i)
451             i += 1
452
453         while self.list.GetItemCount() > i:
454             self.list.DeleteItem(self.list.GetItemCount() - 1)
455
456         self.list.SortListItems(sortstate[0], sortstate[1])
457
458
459 class EdgesPage(wx.Panel):
460     def __init__(self, parent, id):
461         wx.Panel.__init__(self, parent, id)
462         self.list = SuperListCtrl(self, id)
463         self.list.InsertColumn(0, 'From')
464         self.list.InsertColumn(1, 'To')
465         self.list.InsertColumn(2, 'Address')
466         self.list.InsertColumn(3, 'Port')
467         self.list.InsertColumn(4, 'Options')
468         self.list.InsertColumn(5, 'Weight')
469
470         hbox = wx.BoxSizer(wx.HORIZONTAL)
471         hbox.Add(self.list, 1, wx.EXPAND)
472         self.SetSizer(hbox)
473         self.refresh()
474
475     def refresh(self):
476         sortstate = self.list.GetSortState()
477         self.list.itemDataMap = {}
478         i = 0
479
480         for key, edge in vpn.edges.items():
481             if self.list.GetItemCount() <= i:
482                 self.list.InsertStringItem(i, edge.source)
483             else:
484                 self.list.SetStringItem(i, 0, edge.source)
485             self.list.SetStringItem(i, 1, edge.sink)
486             self.list.SetStringItem(i, 2, edge.address)
487             self.list.SetStringItem(i, 3, edge.port)
488             self.list.SetStringItem(i, 4, format(edge.options, "x"))
489             self.list.SetStringItem(i, 5, str(edge.weight))
490             self.list.itemDataMap[i] = (edge.source, edge.sink, edge.address, edge.port, edge.options, edge.weight)
491             self.list.SetItemData(i, i)
492             i += 1
493
494         while self.list.GetItemCount() > i:
495             self.list.DeleteItem(self.list.GetItemCount() - 1)
496
497         self.list.SortListItems(sortstate[0], sortstate[1])
498
499
500 class SubnetsPage(wx.Panel):
501     def __init__(self, parent, id):
502         wx.Panel.__init__(self, parent, id)
503         self.list = SuperListCtrl(self, id)
504         self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
505         self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
506         self.list.InsertColumn(2, 'Owner')
507         hbox = wx.BoxSizer(wx.HORIZONTAL)
508         hbox.Add(self.list, 1, wx.EXPAND)
509         self.SetSizer(hbox)
510         self.refresh()
511
512     def refresh(self):
513         sortstate = self.list.GetSortState()
514         self.list.itemDataMap = {}
515         i = 0
516
517         for key, subnet in vpn.subnets.items():
518             if self.list.GetItemCount() <= i:
519                 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
520             else:
521                 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
522             self.list.SetStringItem(i, 1, str(subnet.weight))
523             self.list.SetStringItem(i, 2, subnet.owner)
524             self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
525             self.list.SetItemData(i, i)
526             i += 1
527
528         while self.list.GetItemCount() > i:
529             self.list.DeleteItem(self.list.GetItemCount() - 1)
530
531         self.list.SortListItems(sortstate[0], sortstate[1])
532
533
534 class StatusPage(wx.Panel):
535     def __init__(self, parent, id):
536         wx.Panel.__init__(self, parent, id)
537
538
539 class GraphPage(wx.Window):
540     def __init__(self, parent, id):
541         wx.Window.__init__(self, parent, id)
542
543
544 class NetPage(wx.Notebook):
545     def __init__(self, parent, id):
546         wx.Notebook.__init__(self, parent)
547         self.settings = SettingsPage(self, id)
548         self.connections = ConnectionsPage(self, id)
549         self.nodes = NodesPage(self, id)
550         self.edges = EdgesPage(self, id)
551         self.subnets = SubnetsPage(self, id)
552         self.graph = GraphPage(self, id)
553         self.status = StatusPage(self, id)
554
555         self.AddPage(self.settings, 'Settings')
556         # self.AddPage(self.status, 'Status')
557         self.AddPage(self.connections, 'Connections')
558         self.AddPage(self.nodes, 'Nodes')
559         self.AddPage(self.edges, 'Edges')
560         self.AddPage(self.subnets, 'Subnets')
561
562         # self.AddPage(self.graph, 'Graph')
563
564
565 class MainWindow(wx.Frame):
566     def __init__(self, parent, id, title):
567         wx.Frame.__init__(self, parent, id, title)
568
569         menubar = wx.MenuBar()
570
571         menu = wx.Menu()
572         menu.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
573         menubar.Append(menu, '&File')
574
575         # nb = wx.Notebook(self, -1)
576         # nb.SetPadding((0, 0))
577         self.np = NetPage(self, -1)
578         # nb.AddPage(np, 'VPN')
579
580         self.timer = wx.Timer(self, -1)
581         self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
582         self.timer.Start(1000)
583         self.Bind(wx.EVT_MENU, self.on_quit, id=1)
584         self.SetMenuBar(menubar)
585         self.Show()
586
587     def on_quit(self, event):
588         app.ExitMainLoop()
589
590     def on_timer(self, event):
591         vpn.refresh()
592         self.np.nodes.refresh()
593         self.np.subnets.refresh()
594         self.np.edges.refresh()
595         self.np.connections.refresh()
596
597
598 def main(netname, pidfile):
599     global vpn, app
600
601     if netname is None:
602         netname = os.getenv('NETNAME')
603
604     vpn = VPN(netname, pidfile)
605     vpn.connect()
606
607     app = wx.App()
608     mw = MainWindow(None, -1, 'Tinc GUI')
609
610     """
611     def OnTaskBarIcon(event):
612         mw.Raise()
613     """
614
615     """
616     icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
617     taskbaricon = wx.TaskBarIcon()
618     taskbaricon.SetIcon(icon, 'Tinc GUI')
619     wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)
620     """
621
622     app.MainLoop()
623     vpn.close()
624
625
626 if __name__ == '__main__':
627     argparser = ArgumentParser(epilog='Report bugs to tinc@tinc-vpn.org.')
628
629     argparser.add_argument('-n', '--net', metavar='NETNAME', dest='netname', help='Connect to net NETNAME')
630     argparser.add_argument('-p', '--pidfile', help='Path to the pid file (containing the controlcookie)')
631
632     options = argparser.parse_args()
633
634     main(options.netname, options.pidfile)