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>
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.
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.
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.
28 from wx.lib.mixins.listctrl import ColumnSorterMixin
29 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
31 if platform.system() == 'Windows':
34 # Classes to interface with a running tinc daemon
42 REQ_DUMP_CONNECTIONS = 6
56 def __init__(self, args):
60 self.address = args[2]
63 self.cipher = int(args[5])
64 self.digest = int(args[6])
65 self.maclength = int(args[7])
67 self.compression = int(args[8])
68 self.options = int(args[9], 0x10)
69 self.status = int(args[10], 0x10)
71 self.nexthop = args[11]
73 self.distance = int(args[13])
74 self.pmtu = int(args[14])
75 self.minmtu = int(args[15])
76 self.maxmtu = int(args[16])
78 self.last_state_change = float(args[17])
84 def __init__(self, args):
88 self.address = args[2]
91 self.options = int(args[-2], 16)
92 self.weight = int(args[-1])
96 def __init__(self, args):
97 if args[0].find('#') >= 0:
98 address, self.weight = args[0].split('#', 1)
103 if address.find('/') >= 0:
104 self.address, self.prefixlen = address.split('/', 1)
106 self.address = address
107 self.prefixlen = '48'
112 class Connection(object):
113 def __init__(self, args):
116 self.address = args[1]
119 self.options = int(args[4], 0x10)
120 self.socket = int(args[5])
121 self.status = int(args[6], 0x10)
127 confdir = '/etc/tinc'
132 f = open(self.pidfile)
133 info = string.split(f.readline())
136 # check if there is a UNIX socket as well
137 if self.pidfile.endswith('.pid'):
138 unixfile = self.pidfile.replace('.pid', '.socket');
140 unixfile = self.pidfile + '.socket';
142 if os.path.exists(unixfile):
143 # use it if it exists
144 print(unixfile + " exists!");
145 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
148 # otherwise connect via TCP
149 print(unixfile + " does not exist.");
154 s = socket.socket(af, socket.SOCK_STREAM)
155 s.connect((info[2], int(info[4])))
157 self.sf = s.makefile()
159 hello = string.split(self.sf.readline())
161 self.sf.write('0 ^' + info[1] + ' 17\r\n')
163 resp = string.split(self.sf.readline())
168 self.connections = {}
172 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
175 for node in self.nodes.values():
177 for edge in self.edges.values():
179 for subnet in self.subnets.values():
180 subnet.visited = False
181 for connections in self.connections.values():
182 connections.visited = False
185 resp = string.split(self.sf.readline())
193 node = self.nodes.get(resp[2]) or Node(resp[2:])
195 self.nodes[resp[2]] = node
199 edge = self.nodes.get((resp[2], resp[3])) or Edge(resp[2:])
201 self.edges[(resp[2], resp[3])] = edge
205 subnet = self.subnets.get((resp[2], resp[3])) or Subnet(resp[2:])
206 subnet.visited = True
207 self.subnets[(resp[2], resp[3])] = subnet
208 if subnet.owner == "(broadcast)":
210 self.nodes[subnet.owner].subnets[resp[2]] = subnet
214 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection(resp[2:])
215 connection.visited = True
216 self.connections[(resp[2], resp[3], resp[5])] = connection
220 for key, subnet in self.subnets.items():
221 if not subnet.visited:
222 del self.subnets[key]
224 for key, edge in self.edges.items():
228 for key, node in self.nodes.items():
232 for key, subnet in node.subnets.items():
233 if not subnet.visited:
234 del node.subnets[key]
236 for key, connection in self.connections.items():
237 if not connection.visited:
238 del self.connections[key]
243 def disconnect(self, name):
244 self.sf.write('18 12 ' + name + '\r\n')
246 resp = string.split(self.sf.readline())
248 def debug(self, level=-1):
249 self.sf.write('18 9 ' + str(level) + '\r\n')
251 resp = string.split(self.sf.readline())
254 def __init__(self, netname=None, pidfile=None):
255 if platform.system() == 'Windows':
256 sam = _winreg.KEY_READ
257 if platform.machine().endswith('64'):
258 sam = sam | _winreg.KEY_WOW64_64KEY
260 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
262 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
264 key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
265 VPN.confdir = _winreg.QueryValue(key, None)
270 self.netname = netname
271 self.confbase = os.path.join(VPN.confdir, netname)
273 self.confbase = VPN.confdir
275 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
277 if pidfile is not None:
278 self.pidfile = pidfile
280 if platform.system() == 'Windows':
281 self.pidfile = os.path.join(self.confbase, 'pid')
284 self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
286 self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
297 def usage(exitcode=0):
298 print('Usage: ' + argv0 + ' [options]')
299 print('\nValid options are:')
300 print(' -n, --net=NETNAME Connect to net NETNAME.')
301 print(' --pidfile=FILENAME Read control cookie from FILENAME.')
302 print(' --help Display this help and exit.')
303 print('\nReport bugs to tinc@tinc-vpn.org.')
308 if sys.argv[0] in ('-n', '--net'):
310 netname = sys.argv[0]
311 elif sys.argv[0] in '--pidfile':
313 pidfile = sys.argv[0]
314 elif sys.argv[0] in '--help':
317 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
323 netname = os.getenv('NETNAME')
327 vpn = VPN(netname, pidfile)
331 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
332 def __init__(self, parent, style):
333 wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
334 ListCtrlAutoWidthMixin.__init__(self)
335 ColumnSorterMixin.__init__(self, 16)
337 def get_list_ctrl(self):
341 class SettingsPage(wx.Panel):
342 def on_debug_level(self, event):
343 vpn.debug(self.debug.GetValue())
345 def __init__(self, parent, id):
346 wx.Panel.__init__(self, parent, id)
347 grid = wx.FlexGridSizer(cols=2)
348 grid.AddGrowableCol(1, 1)
350 namelabel = wx.StaticText(self, -1, 'Name:')
351 self.name = wx.TextCtrl(self, -1, vpn.name)
353 grid.Add(self.name, 1, wx.EXPAND)
355 portlabel = wx.StaticText(self, -1, 'Port:')
356 self.port = wx.TextCtrl(self, -1, vpn.port)
360 debuglabel = wx.StaticText(self, -1, 'Debug level:')
361 self.debug = wx.SpinCtrl(self, min=0, max=5, initial=vpn.debug())
362 self.debug.Bind(wx.EVT_SPINCTRL, self.on_debug_level)
366 modelabel = wx.StaticText(self, -1, 'Mode:')
367 self.mode = wx.ComboBox(self, -1, style=wx.CB_READONLY, value='Router', choices=['Router', 'Switch', 'Hub'])
374 class ConnectionsPage(wx.Panel):
375 def __init__(self, parent, id):
376 wx.Panel.__init__(self, parent, id)
377 self.list = SuperListCtrl(self, id)
378 self.list.InsertColumn(0, 'Name')
379 self.list.InsertColumn(1, 'Address')
380 self.list.InsertColumn(2, 'Port')
381 self.list.InsertColumn(3, 'Options')
382 self.list.InsertColumn(4, 'Weight')
384 hbox = wx.BoxSizer(wx.HORIZONTAL)
385 hbox.Add(self.list, 1, wx.EXPAND)
389 class ContextMenu(wx.Menu):
390 def __init__(self, item):
391 wx.Menu.__init__(self)
395 disconnect = wx.MenuItem(self, -1, 'Disconnect')
396 self.AppendItem(disconnect)
397 self.Bind(wx.EVT_MENU, self.on_disconnect, id=disconnect.GetId())
399 def on_disconnect(self, event):
400 vpn.disconnect(self.item[0])
402 def on_context(self, event):
403 idx = event.GetIndex()
404 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
407 sortstate = self.list.GetSortState()
408 self.list.itemDataMap = {}
411 for key, connection in vpn.connections.items():
412 if self.list.GetItemCount() <= i:
413 self.list.InsertStringItem(i, connection.name)
415 self.list.SetStringItem(i, 0, connection.name)
416 self.list.SetStringItem(i, 1, connection.address)
417 self.list.SetStringItem(i, 2, connection.port)
418 self.list.SetStringItem(i, 3, str(connection.options))
419 self.list.SetStringItem(i, 4, str(connection.weight))
420 self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options,
422 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_context)
423 self.list.SetItemData(i, i)
426 while self.list.GetItemCount() > i:
427 self.list.DeleteItem(self.list.GetItemCount() - 1)
429 self.list.SortListItems(sortstate[0], sortstate[1])
432 class NodesPage(wx.Panel):
433 def __init__(self, parent, id):
434 wx.Panel.__init__(self, parent, id)
435 self.list = SuperListCtrl(self, id)
436 self.list.InsertColumn(0, 'Name')
437 self.list.InsertColumn(1, 'Address')
438 self.list.InsertColumn(2, 'Port')
439 self.list.InsertColumn(3, 'Cipher')
440 self.list.InsertColumn(4, 'Digest')
441 self.list.InsertColumn(5, 'MACLength')
442 self.list.InsertColumn(6, 'Compression')
443 self.list.InsertColumn(7, 'Options')
444 self.list.InsertColumn(8, 'Status')
445 self.list.InsertColumn(9, 'Nexthop')
446 self.list.InsertColumn(10, 'Via')
447 self.list.InsertColumn(11, 'Distance')
448 self.list.InsertColumn(12, 'PMTU')
449 self.list.InsertColumn(13, 'Min MTU')
450 self.list.InsertColumn(14, 'Max MTU')
451 self.list.InsertColumn(15, 'Since')
453 hbox = wx.BoxSizer(wx.HORIZONTAL)
454 hbox.Add(self.list, 1, wx.EXPAND)
459 sortstate = self.list.GetSortState()
460 self.list.itemDataMap = {}
463 for key, node in vpn.nodes.items():
464 if self.list.GetItemCount() <= i:
465 self.list.InsertStringItem(i, node.name)
467 self.list.SetStringItem(i, 0, node.name)
468 self.list.SetStringItem(i, 1, node.address)
469 self.list.SetStringItem(i, 2, node.port)
470 self.list.SetStringItem(i, 3, str(node.cipher))
471 self.list.SetStringItem(i, 4, str(node.digest))
472 self.list.SetStringItem(i, 5, str(node.maclength))
473 self.list.SetStringItem(i, 6, str(node.compression))
474 self.list.SetStringItem(i, 7, format(node.options, "x"))
475 self.list.SetStringItem(i, 8, format(node.status, "04x"))
476 self.list.SetStringItem(i, 9, node.nexthop)
477 self.list.SetStringItem(i, 10, node.via)
478 self.list.SetStringItem(i, 11, str(node.distance))
479 self.list.SetStringItem(i, 12, str(node.pmtu))
480 self.list.SetStringItem(i, 13, str(node.minmtu))
481 self.list.SetStringItem(i, 14, str(node.maxmtu))
482 if node.last_state_change:
483 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
486 self.list.SetStringItem(i, 15, since)
487 self.list.itemDataMap[i] = (node.name, node.address, node.port, node.cipher, node.digest, node.maclength,
488 node.compression, node.options, node.status, node.nexthop, node.via,
489 node.distance, node.pmtu, node.minmtu, node.maxmtu, since)
490 self.list.SetItemData(i, i)
493 while self.list.GetItemCount() > i:
494 self.list.DeleteItem(self.list.GetItemCount() - 1)
496 self.list.SortListItems(sortstate[0], sortstate[1])
499 class EdgesPage(wx.Panel):
500 def __init__(self, parent, id):
501 wx.Panel.__init__(self, parent, id)
502 self.list = SuperListCtrl(self, id)
503 self.list.InsertColumn(0, 'From')
504 self.list.InsertColumn(1, 'To')
505 self.list.InsertColumn(2, 'Address')
506 self.list.InsertColumn(3, 'Port')
507 self.list.InsertColumn(4, 'Options')
508 self.list.InsertColumn(5, 'Weight')
510 hbox = wx.BoxSizer(wx.HORIZONTAL)
511 hbox.Add(self.list, 1, wx.EXPAND)
516 sortstate = self.list.GetSortState()
517 self.list.itemDataMap = {}
520 for key, edge in vpn.edges.items():
521 if self.list.GetItemCount() <= i:
522 self.list.InsertStringItem(i, edge.source)
524 self.list.SetStringItem(i, 0, edge.source)
525 self.list.SetStringItem(i, 1, edge.sink)
526 self.list.SetStringItem(i, 2, edge.address)
527 self.list.SetStringItem(i, 3, edge.port)
528 self.list.SetStringItem(i, 4, format(edge.options, "x"))
529 self.list.SetStringItem(i, 5, str(edge.weight))
530 self.list.itemDataMap[i] = (edge.source, edge.sink, edge.address, edge.port, edge.options, edge.weight)
531 self.list.SetItemData(i, i)
534 while self.list.GetItemCount() > i:
535 self.list.DeleteItem(self.list.GetItemCount() - 1)
537 self.list.SortListItems(sortstate[0], sortstate[1])
540 class SubnetsPage(wx.Panel):
541 def __init__(self, parent, id):
542 wx.Panel.__init__(self, parent, id)
543 self.list = SuperListCtrl(self, id)
544 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
545 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
546 self.list.InsertColumn(2, 'Owner')
547 hbox = wx.BoxSizer(wx.HORIZONTAL)
548 hbox.Add(self.list, 1, wx.EXPAND)
553 sortstate = self.list.GetSortState()
554 self.list.itemDataMap = {}
557 for key, subnet in vpn.subnets.items():
558 if self.list.GetItemCount() <= i:
559 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
561 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
562 self.list.SetStringItem(i, 1, str(subnet.weight))
563 self.list.SetStringItem(i, 2, subnet.owner)
564 self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
565 self.list.SetItemData(i, i)
568 while self.list.GetItemCount() > i:
569 self.list.DeleteItem(self.list.GetItemCount() - 1)
571 self.list.SortListItems(sortstate[0], sortstate[1])
574 class StatusPage(wx.Panel):
575 def __init__(self, parent, id):
576 wx.Panel.__init__(self, parent, id)
579 class GraphPage(wx.Window):
580 def __init__(self, parent, id):
581 wx.Window.__init__(self, parent, id)
584 class NetPage(wx.Notebook):
585 def __init__(self, parent, id):
586 wx.Notebook.__init__(self, parent)
587 self.settings = SettingsPage(self, id)
588 self.connections = ConnectionsPage(self, id)
589 self.nodes = NodesPage(self, id)
590 self.edges = EdgesPage(self, id)
591 self.subnets = SubnetsPage(self, id)
592 self.graph = GraphPage(self, id)
593 self.status = StatusPage(self, id)
595 self.AddPage(self.settings, 'Settings')
596 # self.AddPage(self.status, 'Status')
597 self.AddPage(self.connections, 'Connections')
598 self.AddPage(self.nodes, 'Nodes')
599 self.AddPage(self.edges, 'Edges')
600 self.AddPage(self.subnets, 'Subnets')
602 # self.AddPage(self.graph, 'Graph')
605 class MainWindow(wx.Frame):
606 def on_quit(self, event):
609 def on_timer(self, event):
611 self.np.nodes.refresh()
612 self.np.subnets.refresh()
613 self.np.edges.refresh()
614 self.np.connections.refresh()
616 def __init__(self, parent, id, title):
617 wx.Frame.__init__(self, parent, id, title)
619 menubar = wx.MenuBar()
622 menu.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
623 menubar.Append(menu, '&File')
625 # nb = wx.Notebook(self, -1)
626 # nb.SetPadding((0, 0))
627 self.np = NetPage(self, -1)
628 # nb.AddPage(np, 'VPN')
630 self.timer = wx.Timer(self, -1)
631 self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
632 self.timer.Start(1000)
633 self.Bind(wx.EVT_MENU, self.on_quit, id=1)
634 self.SetMenuBar(menubar)
639 mw = MainWindow(None, -1, 'Tinc GUI')
642 def OnTaskBarIcon(event):
647 icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
648 taskbaricon = wx.TaskBarIcon()
649 taskbaricon.SetIcon(icon, 'Tinc GUI')
650 wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)