3 # tinc-gui -- GUI for controlling a running tincd
4 # Copyright (C) 2009-2012 Guus Sliepen <guus@tinc-vpn.org>
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.
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.
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.
27 from wx.lib.mixins.listctrl import ColumnSorterMixin
28 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
30 if platform.system() == 'Windows':
33 # Classes to interface with a running tinc daemon
41 REQ_DUMP_CONNECTIONS = 6
54 def parse(self, args):
56 self.address = args[1]
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]
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])
75 def parse(self, args):
78 self.address = args[2]
80 self.options = int(args[5], 16)
81 self.weight = int(args[6])
84 def parse(self, args):
85 if args[0].find('#') >= 0:
86 (address, self.weight) = args[0].split('#', 1)
91 if address.find('/') >= 0:
92 (self.address, self.prefixlen) = address.split('/', 1)
94 self.address = address
100 def parse(self, args):
102 self.address = args[1]
104 self.options = int(args[4], 0x10)
105 self.socket = int(args[5])
106 self.status = int(args[6], 0x10)
110 confdir = '/etc/tinc'
115 f = open(self.pidfile)
116 info = string.split(f.readline())
119 # check if there is a UNIX socket as well
120 if self.pidfile.endswith(".pid"):
121 unixfile = self.pidfile.replace(".pid", ".socket");
123 unixfile = self.pidfile + ".socket";
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)
131 # otherwise connect via TCP
132 print(unixfile + " does not exist.");
137 s = socket.socket(af, socket.SOCK_STREAM)
138 s.connect((info[2], int(info[4])))
140 self.sf = s.makefile()
142 hello = string.split(self.sf.readline())
144 self.sf.write('0 ^' + info[1] + ' 17\r\n')
146 resp = string.split(self.sf.readline())
151 self.connections = {}
155 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
158 for node in self.nodes.values():
160 for edge in self.edges.values():
162 for subnet in self.subnets.values():
163 subnet.visited = False
164 for connections in self.connections.values():
165 connections.visited = False
168 resp = string.split(self.sf.readline())
176 node = self.nodes.get(resp[2]) or Node()
179 self.nodes[resp[2]] = node
183 edge = self.nodes.get((resp[2], resp[3])) or Edge()
186 self.edges[(resp[2], resp[3])] = edge
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
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
205 for key, subnet in self.subnets.items():
206 if not subnet.visited:
207 del self.subnets[key]
209 for key, edge in self.edges.items():
213 for key, node in self.nodes.items():
217 for key, subnet in node.subnets.items():
218 if not subnet.visited:
219 del node.subnets[key]
221 for key, connection in self.connections.items():
222 if not connection.visited:
223 del self.connections[key]
228 def disconnect(self, name):
229 self.sf.write('18 12 ' + name + '\r\n')
231 resp = string.split(self.sf.readline())
233 def debug(self, level = -1):
234 self.sf.write('18 9 ' + str(level) + '\r\n')
236 resp = string.split(self.sf.readline())
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
245 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
247 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
249 key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
250 VPN.confdir = _winreg.QueryValue(key, None)
255 self.netname = netname
256 self.confbase = os.path.join(VPN.confdir, netname)
258 self.confbase = VPN.confdir
260 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
263 self.pidfile = pidfile
265 if platform.system() == 'Windows':
266 self.pidfile = os.path.join(self.confbase, 'pid')
269 self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
271 self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
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.')
290 if sys.argv[0] in ('-n', '--net'):
292 netname = sys.argv[0]
293 elif sys.argv[0] in ('--pidfile'):
295 pidfile = sys.argv[0]
296 elif sys.argv[0] in ('--help'):
299 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
305 netname = os.getenv("NETNAME")
310 vpn = VPN(netname, pidfile)
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)
319 def GetListCtrl(self):
323 class SettingsPage(wx.Panel):
324 def OnDebugLevel(self, event):
325 vpn.debug(self.debug.GetValue())
327 def __init__(self, parent, id):
328 wx.Panel.__init__(self, parent, id)
329 grid = wx.FlexGridSizer(cols = 2)
330 grid.AddGrowableCol(1, 1)
332 namelabel = wx.StaticText(self, -1, 'Name:')
333 self.name = wx.TextCtrl(self, -1, vpn.name)
335 grid.Add(self.name, 1, wx.EXPAND)
337 portlabel = wx.StaticText(self, -1, 'Port:')
338 self.port = wx.TextCtrl(self, -1, vpn.port)
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)
348 modelabel = wx.StaticText(self, -1, 'Mode:')
349 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
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')
365 hbox = wx.BoxSizer(wx.HORIZONTAL)
366 hbox.Add(self.list, 1, wx.EXPAND)
370 class ContextMenu(wx.Menu):
371 def __init__(self, item):
372 wx.Menu.__init__(self)
376 disconnect = wx.MenuItem(self, -1, 'Disconnect')
377 self.AppendItem(disconnect)
378 self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
380 def OnDisconnect(self, event):
381 vpn.disconnect(self.item[0])
383 def OnContext(self, event):
385 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
388 sortstate = self.list.GetSortState()
389 self.list.itemDataMap = {}
392 for key, connection in vpn.connections.items():
393 if self.list.GetItemCount() <= i:
394 self.list.InsertStringItem(i, connection.name)
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)
406 while self.list.GetItemCount() > i:
407 self.list.DeleteItem(self.list.GetItemCount() - 1)
409 self.list.SortListItems(sortstate[0], sortstate[1])
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')
432 hbox = wx.BoxSizer(wx.HORIZONTAL)
433 hbox.Add(self.list, 1, wx.EXPAND)
438 sortstate = self.list.GetSortState()
439 self.list.itemDataMap = {}
442 for key, node in vpn.nodes.items():
443 if self.list.GetItemCount() <= i:
444 self.list.InsertStringItem(i, node.name)
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))
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)
470 while self.list.GetItemCount() > i:
471 self.list.DeleteItem(self.list.GetItemCount() - 1)
473 self.list.SortListItems(sortstate[0], sortstate[1])
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')
486 hbox = wx.BoxSizer(wx.HORIZONTAL)
487 hbox.Add(self.list, 1, wx.EXPAND)
492 sortstate = self.list.GetSortState()
493 self.list.itemDataMap = {}
496 for key, edge in vpn.edges.items():
497 if self.list.GetItemCount() <= i:
498 self.list.InsertStringItem(i, edge.fr)
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)
510 while self.list.GetItemCount() > i:
511 self.list.DeleteItem(self.list.GetItemCount() - 1)
513 self.list.SortListItems(sortstate[0], sortstate[1])
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)
528 sortstate = self.list.GetSortState()
529 self.list.itemDataMap = {}
532 for key, subnet in vpn.subnets.items():
533 if self.list.GetItemCount() <= i:
534 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
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)
543 while self.list.GetItemCount() > i:
544 self.list.DeleteItem(self.list.GetItemCount() - 1)
546 self.list.SortListItems(sortstate[0], sortstate[1])
548 class StatusPage(wx.Panel):
549 def __init__(self, parent, id):
550 wx.Panel.__init__(self, parent, id)
552 class GraphPage(wx.Window):
553 def __init__(self, parent, id):
554 wx.Window.__init__(self, parent, id)
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)
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')
576 class MainWindow(wx.Frame):
577 def OnQuit(self, event):
580 def OnTimer(self, event):
582 self.np.nodes.refresh()
583 self.np.subnets.refresh()
584 self.np.edges.refresh()
585 self.np.connections.refresh()
587 def __init__(self, parent, id, title):
588 wx.Frame.__init__(self, parent, id, title)
590 menubar = wx.MenuBar()
592 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
593 menubar.Append(file, '&File')
595 #nb = wx.Notebook(self, -1)
596 #nb.SetPadding((0, 0))
597 self.np = NetPage(self, -1)
598 #nb.AddPage(np, 'VPN')
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)
608 mw = MainWindow(None, -1, 'Tinc GUI')
610 #def OnTaskBarIcon(event):
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)