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