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