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.
26 from argparse import ArgumentParser
29 from wx.lib.mixins.listctrl import ColumnSorterMixin
30 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
32 if platform.system() == 'Windows':
35 # 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 def __init__(self, netname=None, pidfile=None, confdir='/etc/tinc', piddir='/run'):
128 if platform.system() == 'Windows':
129 sam = _winreg.KEY_READ
130 if platform.machine().endswith('64'):
131 sam = sam | _winreg.KEY_WOW64_64KEY
133 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
135 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
137 key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
138 confdir = _winreg.QueryValue(key, None)
143 self.netname = netname
144 self.confbase = os.path.join(confdir, netname)
146 self.confbase = confdir
148 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
150 if pidfile is not None:
151 self.pidfile = pidfile
153 if platform.system() == 'Windows':
154 self.pidfile = os.path.join(self.confbase, 'pid')
157 self.pidfile = os.path.join(piddir, 'tinc.' + netname + '.pid')
159 self.pidfile = os.path.join(piddir, 'tinc.pid')
167 self.connections = {}
171 f = open(self.pidfile)
172 info = string.split(f.readline())
175 # check if there is a UNIX socket as well
176 if self.pidfile.endswith('.pid'):
177 unixfile = self.pidfile.replace('.pid', '.socket');
179 unixfile = self.pidfile + '.socket';
181 if os.path.exists(unixfile):
182 # use it if it exists
183 print(unixfile + " exists!");
184 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
187 # otherwise connect via TCP
188 print(unixfile + " does not exist.");
193 s = socket.socket(af, socket.SOCK_STREAM)
194 s.connect((info[2], int(info[4])))
196 self.sf = s.makefile()
198 hello = string.split(self.sf.readline())
200 self.sf.write('0 ^' + info[1] + ' 17\r\n')
202 resp = string.split(self.sf.readline())
207 for request in (REQ_DUMP_NODES, REQ_DUMP_EDGES, REQ_DUMP_SUBNETS, REQ_DUMP_CONNECTIONS):
208 self.sf.write('{} {}\r\n'.format(CONTROL, request))
211 for node in self.nodes.values():
213 for edge in self.edges.values():
215 for subnet in self.subnets.values():
216 subnet.visited = False
217 for connections in self.connections.values():
218 connections.visited = False
221 resp = string.split(self.sf.readline())
229 node = self.nodes.get(resp[2]) or Node(resp[2:])
231 self.nodes[resp[2]] = node
235 edge = self.nodes.get((resp[2], resp[3])) or Edge(resp[2:])
237 self.edges[(resp[2], resp[3])] = edge
241 subnet = self.subnets.get((resp[2], resp[3])) or Subnet(resp[2:])
242 subnet.visited = True
243 self.subnets[(resp[2], resp[3])] = subnet
244 if subnet.owner == "(broadcast)":
246 self.nodes[subnet.owner].subnets[resp[2]] = subnet
250 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection(resp[2:])
251 connection.visited = True
252 self.connections[(resp[2], resp[3], resp[5])] = connection
256 for key, subnet in self.subnets.items():
257 if not subnet.visited:
258 del self.subnets[key]
260 for key, edge in self.edges.items():
264 for key, node in self.nodes.items():
268 for key, subnet in node.subnets.items():
269 if not subnet.visited:
270 del node.subnets[key]
272 for key, connection in self.connections.items():
273 if not connection.visited:
274 del self.connections[key]
279 def disconnect(self, name):
280 self.sf.write('18 12 ' + name + '\r\n')
282 resp = string.split(self.sf.readline())
284 def debug(self, level=-1):
285 self.sf.write('18 9 ' + str(level) + '\r\n')
287 resp = string.split(self.sf.readline())
291 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
292 def __init__(self, parent, style):
293 wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
294 ListCtrlAutoWidthMixin.__init__(self)
295 ColumnSorterMixin.__init__(self, 16)
297 def GetListCtrl(self):
301 class SettingsPage(wx.Panel):
302 def on_debug_level(self, event):
303 vpn.debug(self.debug.GetValue())
305 def __init__(self, parent, id):
306 wx.Panel.__init__(self, parent, id)
307 grid = wx.FlexGridSizer(cols=2)
308 grid.AddGrowableCol(1, 1)
310 namelabel = wx.StaticText(self, -1, 'Name:')
311 self.name = wx.TextCtrl(self, -1, vpn.name)
313 grid.Add(self.name, 1, wx.EXPAND)
315 portlabel = wx.StaticText(self, -1, 'Port:')
316 self.port = wx.TextCtrl(self, -1, vpn.port)
320 debuglabel = wx.StaticText(self, -1, 'Debug level:')
321 self.debug = wx.SpinCtrl(self, min=0, max=5, initial=vpn.debug())
322 self.debug.Bind(wx.EVT_SPINCTRL, self.on_debug_level)
326 modelabel = wx.StaticText(self, -1, 'Mode:')
327 self.mode = wx.ComboBox(self, -1, style=wx.CB_READONLY, value='Router', choices=['Router', 'Switch', 'Hub'])
334 class ConnectionsPage(wx.Panel):
335 def __init__(self, parent, id):
336 wx.Panel.__init__(self, parent, id)
337 self.list = SuperListCtrl(self, id)
338 self.list.InsertColumn(0, 'Name')
339 self.list.InsertColumn(1, 'Address')
340 self.list.InsertColumn(2, 'Port')
341 self.list.InsertColumn(3, 'Options')
342 self.list.InsertColumn(4, 'Weight')
344 hbox = wx.BoxSizer(wx.HORIZONTAL)
345 hbox.Add(self.list, 1, wx.EXPAND)
349 class ContextMenu(wx.Menu):
350 def __init__(self, item):
351 wx.Menu.__init__(self)
355 disconnect = wx.MenuItem(self, -1, 'Disconnect')
356 self.AppendItem(disconnect)
357 self.Bind(wx.EVT_MENU, self.on_disconnect, id=disconnect.GetId())
359 def on_disconnect(self, event):
360 vpn.disconnect(self.item[0])
362 def on_context(self, event):
363 idx = event.GetIndex()
364 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
367 sortstate = self.list.GetSortState()
368 self.list.itemDataMap = {}
371 for key, connection in vpn.connections.items():
372 if self.list.GetItemCount() <= i:
373 self.list.InsertStringItem(i, connection.name)
375 self.list.SetStringItem(i, 0, connection.name)
376 self.list.SetStringItem(i, 1, connection.address)
377 self.list.SetStringItem(i, 2, connection.port)
378 self.list.SetStringItem(i, 3, str(connection.options))
379 self.list.SetStringItem(i, 4, str(connection.weight))
380 self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options,
382 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_context)
383 self.list.SetItemData(i, i)
386 while self.list.GetItemCount() > i:
387 self.list.DeleteItem(self.list.GetItemCount() - 1)
389 self.list.SortListItems(sortstate[0], sortstate[1])
392 class NodesPage(wx.Panel):
393 def __init__(self, parent, id):
394 wx.Panel.__init__(self, parent, id)
395 self.list = SuperListCtrl(self, id)
396 self.list.InsertColumn(0, 'Name')
397 self.list.InsertColumn(1, 'Address')
398 self.list.InsertColumn(2, 'Port')
399 self.list.InsertColumn(3, 'Cipher')
400 self.list.InsertColumn(4, 'Digest')
401 self.list.InsertColumn(5, 'MACLength')
402 self.list.InsertColumn(6, 'Compression')
403 self.list.InsertColumn(7, 'Options')
404 self.list.InsertColumn(8, 'Status')
405 self.list.InsertColumn(9, 'Nexthop')
406 self.list.InsertColumn(10, 'Via')
407 self.list.InsertColumn(11, 'Distance')
408 self.list.InsertColumn(12, 'PMTU')
409 self.list.InsertColumn(13, 'Min MTU')
410 self.list.InsertColumn(14, 'Max MTU')
411 self.list.InsertColumn(15, 'Since')
413 hbox = wx.BoxSizer(wx.HORIZONTAL)
414 hbox.Add(self.list, 1, wx.EXPAND)
419 sortstate = self.list.GetSortState()
420 self.list.itemDataMap = {}
423 for key, node in vpn.nodes.items():
424 if self.list.GetItemCount() <= i:
425 self.list.InsertStringItem(i, node.name)
427 self.list.SetStringItem(i, 0, node.name)
428 self.list.SetStringItem(i, 1, node.address)
429 self.list.SetStringItem(i, 2, node.port)
430 self.list.SetStringItem(i, 3, str(node.cipher))
431 self.list.SetStringItem(i, 4, str(node.digest))
432 self.list.SetStringItem(i, 5, str(node.maclength))
433 self.list.SetStringItem(i, 6, str(node.compression))
434 self.list.SetStringItem(i, 7, format(node.options, "x"))
435 self.list.SetStringItem(i, 8, format(node.status, "04x"))
436 self.list.SetStringItem(i, 9, node.nexthop)
437 self.list.SetStringItem(i, 10, node.via)
438 self.list.SetStringItem(i, 11, str(node.distance))
439 self.list.SetStringItem(i, 12, str(node.pmtu))
440 self.list.SetStringItem(i, 13, str(node.minmtu))
441 self.list.SetStringItem(i, 14, str(node.maxmtu))
442 if node.last_state_change:
443 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
446 self.list.SetStringItem(i, 15, since)
447 self.list.itemDataMap[i] = (node.name, node.address, node.port, node.cipher, node.digest, node.maclength,
448 node.compression, node.options, node.status, node.nexthop, node.via,
449 node.distance, node.pmtu, node.minmtu, node.maxmtu, since)
450 self.list.SetItemData(i, i)
453 while self.list.GetItemCount() > i:
454 self.list.DeleteItem(self.list.GetItemCount() - 1)
456 self.list.SortListItems(sortstate[0], sortstate[1])
459 class EdgesPage(wx.Panel):
460 def __init__(self, parent, id):
461 wx.Panel.__init__(self, parent, id)
462 self.list = SuperListCtrl(self, id)
463 self.list.InsertColumn(0, 'From')
464 self.list.InsertColumn(1, 'To')
465 self.list.InsertColumn(2, 'Address')
466 self.list.InsertColumn(3, 'Port')
467 self.list.InsertColumn(4, 'Options')
468 self.list.InsertColumn(5, 'Weight')
470 hbox = wx.BoxSizer(wx.HORIZONTAL)
471 hbox.Add(self.list, 1, wx.EXPAND)
476 sortstate = self.list.GetSortState()
477 self.list.itemDataMap = {}
480 for key, edge in vpn.edges.items():
481 if self.list.GetItemCount() <= i:
482 self.list.InsertStringItem(i, edge.source)
484 self.list.SetStringItem(i, 0, edge.source)
485 self.list.SetStringItem(i, 1, edge.sink)
486 self.list.SetStringItem(i, 2, edge.address)
487 self.list.SetStringItem(i, 3, edge.port)
488 self.list.SetStringItem(i, 4, format(edge.options, "x"))
489 self.list.SetStringItem(i, 5, str(edge.weight))
490 self.list.itemDataMap[i] = (edge.source, edge.sink, edge.address, edge.port, edge.options, edge.weight)
491 self.list.SetItemData(i, i)
494 while self.list.GetItemCount() > i:
495 self.list.DeleteItem(self.list.GetItemCount() - 1)
497 self.list.SortListItems(sortstate[0], sortstate[1])
500 class SubnetsPage(wx.Panel):
501 def __init__(self, parent, id):
502 wx.Panel.__init__(self, parent, id)
503 self.list = SuperListCtrl(self, id)
504 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
505 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
506 self.list.InsertColumn(2, 'Owner')
507 hbox = wx.BoxSizer(wx.HORIZONTAL)
508 hbox.Add(self.list, 1, wx.EXPAND)
513 sortstate = self.list.GetSortState()
514 self.list.itemDataMap = {}
517 for key, subnet in vpn.subnets.items():
518 if self.list.GetItemCount() <= i:
519 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
521 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
522 self.list.SetStringItem(i, 1, str(subnet.weight))
523 self.list.SetStringItem(i, 2, subnet.owner)
524 self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
525 self.list.SetItemData(i, i)
528 while self.list.GetItemCount() > i:
529 self.list.DeleteItem(self.list.GetItemCount() - 1)
531 self.list.SortListItems(sortstate[0], sortstate[1])
534 class StatusPage(wx.Panel):
535 def __init__(self, parent, id):
536 wx.Panel.__init__(self, parent, id)
539 class GraphPage(wx.Window):
540 def __init__(self, parent, id):
541 wx.Window.__init__(self, parent, id)
544 class NetPage(wx.Notebook):
545 def __init__(self, parent, id):
546 wx.Notebook.__init__(self, parent)
547 self.settings = SettingsPage(self, id)
548 self.connections = ConnectionsPage(self, id)
549 self.nodes = NodesPage(self, id)
550 self.edges = EdgesPage(self, id)
551 self.subnets = SubnetsPage(self, id)
552 self.graph = GraphPage(self, id)
553 self.status = StatusPage(self, id)
555 self.AddPage(self.settings, 'Settings')
556 # self.AddPage(self.status, 'Status')
557 self.AddPage(self.connections, 'Connections')
558 self.AddPage(self.nodes, 'Nodes')
559 self.AddPage(self.edges, 'Edges')
560 self.AddPage(self.subnets, 'Subnets')
562 # self.AddPage(self.graph, 'Graph')
565 class MainWindow(wx.Frame):
566 def __init__(self, parent, id, title):
567 wx.Frame.__init__(self, parent, id, title)
569 menubar = wx.MenuBar()
572 menu.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
573 menubar.Append(menu, '&File')
575 # nb = wx.Notebook(self, -1)
576 # nb.SetPadding((0, 0))
577 self.np = NetPage(self, -1)
578 # nb.AddPage(np, 'VPN')
580 self.timer = wx.Timer(self, -1)
581 self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
582 self.timer.Start(1000)
583 self.Bind(wx.EVT_MENU, self.on_quit, id=1)
584 self.SetMenuBar(menubar)
587 def on_quit(self, event):
590 def on_timer(self, event):
592 self.np.nodes.refresh()
593 self.np.subnets.refresh()
594 self.np.edges.refresh()
595 self.np.connections.refresh()
598 def main(netname, pidfile):
602 netname = os.getenv('NETNAME')
604 vpn = VPN(netname, pidfile)
608 mw = MainWindow(None, -1, 'Tinc GUI')
611 def OnTaskBarIcon(event):
616 icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
617 taskbaricon = wx.TaskBarIcon()
618 taskbaricon.SetIcon(icon, 'Tinc GUI')
619 wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)
626 if __name__ == '__main__':
627 argparser = ArgumentParser(epilog='Report bugs to tinc@tinc-vpn.org.')
629 argparser.add_argument('-n', '--net', metavar='NETNAME', dest='netname', help='Connect to net NETNAME')
630 argparser.add_argument('-p', '--pidfile', help='Path to the pid file (containing the controlcookie)')
632 options = argparser.parse_args()
634 main(options.netname, options.pidfile)