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.
27 from argparse import ArgumentParser
30 from wx.lib.mixins.listctrl import ColumnSorterMixin
31 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
33 if platform.system() == 'Windows':
36 # Classes to interface with a running tinc daemon
43 REQ_DUMP_CONNECTIONS = 6
57 def __init__(self, args):
61 self.address = args[2]
64 self.cipher = int(args[5])
65 self.digest = int(args[6])
66 self.maclength = int(args[7])
68 self.compression = int(args[8])
69 self.options = int(args[9], 0x10)
70 self.status = int(args[10], 0x10)
72 self.nexthop = args[11]
74 self.distance = int(args[13])
75 self.pmtu = int(args[14])
76 self.minmtu = int(args[15])
77 self.maxmtu = int(args[16])
79 self.last_state_change = float(args[17])
85 def __init__(self, args):
89 self.address = args[2]
92 self.options = int(args[-2], 16)
93 self.weight = int(args[-1])
97 def __init__(self, args):
98 if args[0].find('#') >= 0:
99 address, self.weight = args[0].split('#', 1)
104 if address.find('/') >= 0:
105 self.address, self.prefixlen = address.split('/', 1)
107 self.address = address
108 self.prefixlen = '48'
113 class Connection(object):
114 def __init__(self, args):
117 self.address = args[1]
120 self.options = int(args[4], 0x10)
121 self.socket = int(args[5])
122 self.status = int(args[6], 0x10)
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
134 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
136 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
138 key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
139 confdir = _winreg.QueryValue(key, None)
144 self.netname = netname
145 self.confbase = os.path.join(confdir, netname)
147 self.confbase = confdir
149 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
151 if pidfile is not None:
152 self.pidfile = pidfile
154 if platform.system() == 'Windows':
155 self.pidfile = os.path.join(self.confbase, 'pid')
158 self.pidfile = os.path.join(piddir, 'tinc.' + netname + '.pid')
160 self.pidfile = os.path.join(piddir, 'tinc.pid')
168 self.connections = {}
172 f = open(self.pidfile)
173 info = string.split(f.readline())
176 # check if there is a UNIX socket as well
177 if self.pidfile.endswith('.pid'):
178 unixfile = self.pidfile.replace('.pid', '.socket');
180 unixfile = self.pidfile + '.socket';
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)
188 # otherwise connect via TCP
189 print(unixfile + " does not exist.");
194 s = socket.socket(af, socket.SOCK_STREAM)
195 s.connect((info[2], int(info[4])))
197 self.sf = s.makefile()
199 hello = string.split(self.sf.readline())
201 self.sf.write('0 ^' + info[1] + ' 17\r\n')
203 resp = string.split(self.sf.readline())
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))
212 for node in self.nodes.values():
214 for edge in self.edges.values():
216 for subnet in self.subnets.values():
217 subnet.visited = False
218 for connections in self.connections.values():
219 connections.visited = False
222 resp = string.split(self.sf.readline())
230 node = self.nodes.get(resp[2]) or Node(resp[2:])
232 self.nodes[resp[2]] = node
236 edge = self.nodes.get((resp[2], resp[3])) or Edge(resp[2:])
238 self.edges[(resp[2], resp[3])] = edge
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)":
247 self.nodes[subnet.owner].subnets[resp[2]] = subnet
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
257 for key, subnet in self.subnets.items():
258 if not subnet.visited:
259 del self.subnets[key]
261 for key, edge in self.edges.items():
265 for key, node in self.nodes.items():
269 for key, subnet in node.subnets.items():
270 if not subnet.visited:
271 del node.subnets[key]
273 for key, connection in self.connections.items():
274 if not connection.visited:
275 del self.connections[key]
280 def disconnect(self, name):
281 self.sf.write('18 12 ' + name + '\r\n')
283 resp = string.split(self.sf.readline())
285 def debug(self, level=-1):
286 self.sf.write('18 9 ' + str(level) + '\r\n')
288 resp = string.split(self.sf.readline())
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)
298 def GetListCtrl(self):
302 class SettingsPage(wx.Panel):
303 def on_debug_level(self, event):
304 vpn.debug(self.debug.GetValue())
306 def __init__(self, parent, id):
307 wx.Panel.__init__(self, parent, id)
308 grid = wx.FlexGridSizer(cols=2)
309 grid.AddGrowableCol(1, 1)
311 namelabel = wx.StaticText(self, -1, 'Name:')
312 self.name = wx.TextCtrl(self, -1, vpn.name)
314 grid.Add(self.name, 1, wx.EXPAND)
316 portlabel = wx.StaticText(self, -1, 'Port:')
317 self.port = wx.TextCtrl(self, -1, vpn.port)
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)
327 modelabel = wx.StaticText(self, -1, 'Mode:')
328 self.mode = wx.ComboBox(self, -1, style=wx.CB_READONLY, value='Router', choices=['Router', 'Switch', 'Hub'])
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')
345 hbox = wx.BoxSizer(wx.HORIZONTAL)
346 hbox.Add(self.list, 1, wx.EXPAND)
350 class ContextMenu(wx.Menu):
351 def __init__(self, item):
352 wx.Menu.__init__(self)
356 disconnect = wx.MenuItem(self, -1, 'Disconnect')
357 self.AppendItem(disconnect)
358 self.Bind(wx.EVT_MENU, self.on_disconnect, id=disconnect.GetId())
360 def on_disconnect(self, event):
361 vpn.disconnect(self.item[0])
363 def on_context(self, event):
364 idx = event.GetIndex()
365 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
368 sortstate = self.list.GetSortState()
369 self.list.itemDataMap = {}
372 for key, connection in vpn.connections.items():
373 if self.list.GetItemCount() <= i:
374 self.list.InsertStringItem(i, connection.name)
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,
383 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_context)
384 self.list.SetItemData(i, i)
387 while self.list.GetItemCount() > i:
388 self.list.DeleteItem(self.list.GetItemCount() - 1)
390 self.list.SortListItems(sortstate[0], sortstate[1])
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')
414 hbox = wx.BoxSizer(wx.HORIZONTAL)
415 hbox.Add(self.list, 1, wx.EXPAND)
420 sortstate = self.list.GetSortState()
421 self.list.itemDataMap = {}
424 for key, node in vpn.nodes.items():
425 if self.list.GetItemCount() <= i:
426 self.list.InsertStringItem(i, node.name)
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))
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)
454 while self.list.GetItemCount() > i:
455 self.list.DeleteItem(self.list.GetItemCount() - 1)
457 self.list.SortListItems(sortstate[0], sortstate[1])
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')
471 hbox = wx.BoxSizer(wx.HORIZONTAL)
472 hbox.Add(self.list, 1, wx.EXPAND)
477 sortstate = self.list.GetSortState()
478 self.list.itemDataMap = {}
481 for key, edge in vpn.edges.items():
482 if self.list.GetItemCount() <= i:
483 self.list.InsertStringItem(i, edge.source)
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)
495 while self.list.GetItemCount() > i:
496 self.list.DeleteItem(self.list.GetItemCount() - 1)
498 self.list.SortListItems(sortstate[0], sortstate[1])
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)
514 sortstate = self.list.GetSortState()
515 self.list.itemDataMap = {}
518 for key, subnet in vpn.subnets.items():
519 if self.list.GetItemCount() <= i:
520 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
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)
529 while self.list.GetItemCount() > i:
530 self.list.DeleteItem(self.list.GetItemCount() - 1)
532 self.list.SortListItems(sortstate[0], sortstate[1])
535 class StatusPage(wx.Panel):
536 def __init__(self, parent, id):
537 wx.Panel.__init__(self, parent, id)
540 class GraphPage(wx.Window):
541 def __init__(self, parent, id):
542 wx.Window.__init__(self, parent, id)
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)
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')
563 # self.AddPage(self.graph, 'Graph')
566 class MainWindow(wx.Frame):
567 def __init__(self, parent, id, title):
568 wx.Frame.__init__(self, parent, id, title)
570 menubar = wx.MenuBar()
573 menu.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
574 menubar.Append(menu, '&File')
576 # nb = wx.Notebook(self, -1)
577 # nb.SetPadding((0, 0))
578 self.np = NetPage(self, -1)
579 # nb.AddPage(np, 'VPN')
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)
588 def on_quit(self, event):
591 def on_timer(self, event):
593 self.np.nodes.refresh()
594 self.np.subnets.refresh()
595 self.np.edges.refresh()
596 self.np.connections.refresh()
599 def main(netname, pidfile):
603 netname = os.getenv('NETNAME')
605 vpn = VPN(netname, pidfile)
609 mw = MainWindow(None, -1, 'Tinc GUI')
612 def OnTaskBarIcon(event):
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)
627 if __name__ == '__main__':
628 argparser = ArgumentParser(epilog='Report bugs to tinc@tinc-vpn.org.')
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')
634 options = argparser.parse_args()
637 print('tinc-gui 1.1pre?')
640 main(options.netname, options.pidfile)