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