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