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):
58 self.address = args[1]
60 self.cipher = int(args[4])
61 self.digest = int(args[5])
62 self.maclength = int(args[6])
63 self.compression = int(args[7])
64 self.options = int(args[8], 0x10)
65 self.status = int(args[9], 0x10)
66 self.nexthop = args[10]
68 self.distance = int(args[12])
69 self.pmtu = int(args[13])
70 self.minmtu = int(args[14])
71 self.maxmtu = int(args[15])
72 self.last_state_change = float(args[16])
78 def __init__(self, args):
81 self.address = args[2]
83 self.options = int(args[-2], 16)
84 self.weight = int(args[-1])
88 def __init__(self, args):
89 if args[0].find('#') >= 0:
90 (address, self.weight) = args[0].split('#', 1)
95 if address.find('/') >= 0:
96 (self.address, self.prefixlen) = address.split('/', 1)
98 self.address = address
104 class Connection(object):
105 def __init__(self, args):
107 self.address = args[1]
109 self.options = int(args[4], 0x10)
110 self.socket = int(args[5])
111 self.status = int(args[6], 0x10)
116 confdir = '/etc/tinc'
121 f = open(self.pidfile)
122 info = string.split(f.readline())
125 # check if there is a UNIX socket as well
126 if self.pidfile.endswith('.pid'):
127 unixfile = self.pidfile.replace('.pid', '.socket');
129 unixfile = self.pidfile + '.socket';
131 if os.path.exists(unixfile):
132 # use it if it exists
133 print(unixfile + " exists!");
134 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
137 # otherwise connect via TCP
138 print(unixfile + " does not exist.");
143 s = socket.socket(af, socket.SOCK_STREAM)
144 s.connect((info[2], int(info[4])))
146 self.sf = s.makefile()
148 hello = string.split(self.sf.readline())
150 self.sf.write('0 ^' + info[1] + ' 17\r\n')
152 resp = string.split(self.sf.readline())
157 self.connections = {}
161 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
164 for node in self.nodes.values():
166 for edge in self.edges.values():
168 for subnet in self.subnets.values():
169 subnet.visited = False
170 for connections in self.connections.values():
171 connections.visited = False
174 resp = string.split(self.sf.readline())
182 node = self.nodes.get(resp[2]) or Node(resp[2:])
184 self.nodes[resp[2]] = node
188 edge = self.nodes.get((resp[2], resp[3])) or Edge(resp[2:])
190 self.edges[(resp[2], resp[3])] = edge
194 subnet = self.subnets.get((resp[2], resp[3])) or Subnet(resp[2:])
195 subnet.visited = True
196 self.subnets[(resp[2], resp[3])] = subnet
197 if subnet.owner == "(broadcast)":
199 self.nodes[subnet.owner].subnets[resp[2]] = subnet
203 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection(resp[2:])
204 connection.visited = True
205 self.connections[(resp[2], resp[3], resp[5])] = connection
209 for key, subnet in self.subnets.items():
210 if not subnet.visited:
211 del self.subnets[key]
213 for key, edge in self.edges.items():
217 for key, node in self.nodes.items():
221 for key, subnet in node.subnets.items():
222 if not subnet.visited:
223 del node.subnets[key]
225 for key, connection in self.connections.items():
226 if not connection.visited:
227 del self.connections[key]
232 def disconnect(self, name):
233 self.sf.write('18 12 ' + name + '\r\n')
235 resp = string.split(self.sf.readline())
237 def debug(self, level=-1):
238 self.sf.write('18 9 ' + str(level) + '\r\n')
240 resp = string.split(self.sf.readline())
243 def __init__(self, netname=None, pidfile=None):
244 if platform.system() == 'Windows':
245 sam = _winreg.KEY_READ
246 if platform.machine().endswith('64'):
247 sam = sam | _winreg.KEY_WOW64_64KEY
249 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
251 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
253 key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
254 VPN.confdir = _winreg.QueryValue(key, None)
259 self.netname = netname
260 self.confbase = os.path.join(VPN.confdir, netname)
262 self.confbase = VPN.confdir
264 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
266 if pidfile is not None:
267 self.pidfile = pidfile
269 if platform.system() == 'Windows':
270 self.pidfile = os.path.join(self.confbase, 'pid')
273 self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
275 self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
286 def usage(exitcode=0):
287 print('Usage: ' + argv0 + ' [options]')
288 print('\nValid options are:')
289 print(' -n, --net=NETNAME Connect to net NETNAME.')
290 print(' --pidfile=FILENAME Read control cookie from FILENAME.')
291 print(' --help Display this help and exit.')
292 print('\nReport bugs to tinc@tinc-vpn.org.')
297 if sys.argv[0] in ('-n', '--net'):
299 netname = sys.argv[0]
300 elif sys.argv[0] in '--pidfile':
302 pidfile = sys.argv[0]
303 elif sys.argv[0] in '--help':
306 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
312 netname = os.getenv('NETNAME')
316 vpn = VPN(netname, pidfile)
320 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
321 def __init__(self, parent, style):
322 wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
323 ListCtrlAutoWidthMixin.__init__(self)
324 ColumnSorterMixin.__init__(self, 16)
326 def get_list_ctrl(self):
330 class SettingsPage(wx.Panel):
331 def on_debug_level(self, event):
332 vpn.debug(self.debug.GetValue())
334 def __init__(self, parent, id):
335 wx.Panel.__init__(self, parent, id)
336 grid = wx.FlexGridSizer(cols=2)
337 grid.AddGrowableCol(1, 1)
339 namelabel = wx.StaticText(self, -1, 'Name:')
340 self.name = wx.TextCtrl(self, -1, vpn.name)
342 grid.Add(self.name, 1, wx.EXPAND)
344 portlabel = wx.StaticText(self, -1, 'Port:')
345 self.port = wx.TextCtrl(self, -1, vpn.port)
349 debuglabel = wx.StaticText(self, -1, 'Debug level:')
350 self.debug = wx.SpinCtrl(self, min=0, max=5, initial=vpn.debug())
351 self.debug.Bind(wx.EVT_SPINCTRL, self.on_debug_level)
355 modelabel = wx.StaticText(self, -1, 'Mode:')
356 self.mode = wx.ComboBox(self, -1, style=wx.CB_READONLY, value='Router', choices=['Router', 'Switch', 'Hub'])
363 class ConnectionsPage(wx.Panel):
364 def __init__(self, parent, id):
365 wx.Panel.__init__(self, parent, id)
366 self.list = SuperListCtrl(self, id)
367 self.list.InsertColumn(0, 'Name')
368 self.list.InsertColumn(1, 'Address')
369 self.list.InsertColumn(2, 'Port')
370 self.list.InsertColumn(3, 'Options')
371 self.list.InsertColumn(4, 'Weight')
373 hbox = wx.BoxSizer(wx.HORIZONTAL)
374 hbox.Add(self.list, 1, wx.EXPAND)
378 class ContextMenu(wx.Menu):
379 def __init__(self, item):
380 wx.Menu.__init__(self)
384 disconnect = wx.MenuItem(self, -1, 'Disconnect')
385 self.AppendItem(disconnect)
386 self.Bind(wx.EVT_MENU, self.on_disconnect, id=disconnect.GetId())
388 def on_disconnect(self, event):
389 vpn.disconnect(self.item[0])
391 def on_context(self, event):
392 idx = event.GetIndex()
393 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
396 sortstate = self.list.GetSortState()
397 self.list.itemDataMap = {}
400 for key, connection in vpn.connections.items():
401 if self.list.GetItemCount() <= i:
402 self.list.InsertStringItem(i, connection.name)
404 self.list.SetStringItem(i, 0, connection.name)
405 self.list.SetStringItem(i, 1, connection.address)
406 self.list.SetStringItem(i, 2, connection.port)
407 self.list.SetStringItem(i, 3, str(connection.options))
408 self.list.SetStringItem(i, 4, str(connection.weight))
409 self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options,
411 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_context)
412 self.list.SetItemData(i, i)
415 while self.list.GetItemCount() > i:
416 self.list.DeleteItem(self.list.GetItemCount() - 1)
418 self.list.SortListItems(sortstate[0], sortstate[1])
421 class NodesPage(wx.Panel):
422 def __init__(self, parent, id):
423 wx.Panel.__init__(self, parent, id)
424 self.list = SuperListCtrl(self, id)
425 self.list.InsertColumn(0, 'Name')
426 self.list.InsertColumn(1, 'Address')
427 self.list.InsertColumn(2, 'Port')
428 self.list.InsertColumn(3, 'Cipher')
429 self.list.InsertColumn(4, 'Digest')
430 self.list.InsertColumn(5, 'MACLength')
431 self.list.InsertColumn(6, 'Compression')
432 self.list.InsertColumn(7, 'Options')
433 self.list.InsertColumn(8, 'Status')
434 self.list.InsertColumn(9, 'Nexthop')
435 self.list.InsertColumn(10, 'Via')
436 self.list.InsertColumn(11, 'Distance')
437 self.list.InsertColumn(12, 'PMTU')
438 self.list.InsertColumn(13, 'Min MTU')
439 self.list.InsertColumn(14, 'Max MTU')
440 self.list.InsertColumn(15, 'Since')
442 hbox = wx.BoxSizer(wx.HORIZONTAL)
443 hbox.Add(self.list, 1, wx.EXPAND)
448 sortstate = self.list.GetSortState()
449 self.list.itemDataMap = {}
452 for key, node in vpn.nodes.items():
453 if self.list.GetItemCount() <= i:
454 self.list.InsertStringItem(i, node.name)
456 self.list.SetStringItem(i, 0, node.name)
457 self.list.SetStringItem(i, 1, node.address)
458 self.list.SetStringItem(i, 2, node.port)
459 self.list.SetStringItem(i, 3, str(node.cipher))
460 self.list.SetStringItem(i, 4, str(node.digest))
461 self.list.SetStringItem(i, 5, str(node.maclength))
462 self.list.SetStringItem(i, 6, str(node.compression))
463 self.list.SetStringItem(i, 7, format(node.options, "x"))
464 self.list.SetStringItem(i, 8, format(node.status, "04x"))
465 self.list.SetStringItem(i, 9, node.nexthop)
466 self.list.SetStringItem(i, 10, node.via)
467 self.list.SetStringItem(i, 11, str(node.distance))
468 self.list.SetStringItem(i, 12, str(node.pmtu))
469 self.list.SetStringItem(i, 13, str(node.minmtu))
470 self.list.SetStringItem(i, 14, str(node.maxmtu))
471 if node.last_state_change:
472 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
475 self.list.SetStringItem(i, 15, since)
476 self.list.itemDataMap[i] = (node.name, node.address, node.port, node.cipher, node.digest, node.maclength,
477 node.compression, node.options, node.status, node.nexthop, node.via,
478 node.distance, node.pmtu, node.minmtu, node.maxmtu, since)
479 self.list.SetItemData(i, i)
482 while self.list.GetItemCount() > i:
483 self.list.DeleteItem(self.list.GetItemCount() - 1)
485 self.list.SortListItems(sortstate[0], sortstate[1])
488 class EdgesPage(wx.Panel):
489 def __init__(self, parent, id):
490 wx.Panel.__init__(self, parent, id)
491 self.list = SuperListCtrl(self, id)
492 self.list.InsertColumn(0, 'From')
493 self.list.InsertColumn(1, 'To')
494 self.list.InsertColumn(2, 'Address')
495 self.list.InsertColumn(3, 'Port')
496 self.list.InsertColumn(4, 'Options')
497 self.list.InsertColumn(5, 'Weight')
499 hbox = wx.BoxSizer(wx.HORIZONTAL)
500 hbox.Add(self.list, 1, wx.EXPAND)
505 sortstate = self.list.GetSortState()
506 self.list.itemDataMap = {}
509 for key, edge in vpn.edges.items():
510 if self.list.GetItemCount() <= i:
511 self.list.InsertStringItem(i, edge.fr)
513 self.list.SetStringItem(i, 0, edge.fr)
514 self.list.SetStringItem(i, 1, edge.to)
515 self.list.SetStringItem(i, 2, edge.address)
516 self.list.SetStringItem(i, 3, edge.port)
517 self.list.SetStringItem(i, 4, format(edge.options, "x"))
518 self.list.SetStringItem(i, 5, str(edge.weight))
519 self.list.itemDataMap[i] = (edge.fr, edge.to, edge.address, edge.port, edge.options, edge.weight)
520 self.list.SetItemData(i, i)
523 while self.list.GetItemCount() > i:
524 self.list.DeleteItem(self.list.GetItemCount() - 1)
526 self.list.SortListItems(sortstate[0], sortstate[1])
529 class SubnetsPage(wx.Panel):
530 def __init__(self, parent, id):
531 wx.Panel.__init__(self, parent, id)
532 self.list = SuperListCtrl(self, id)
533 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
534 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
535 self.list.InsertColumn(2, 'Owner')
536 hbox = wx.BoxSizer(wx.HORIZONTAL)
537 hbox.Add(self.list, 1, wx.EXPAND)
542 sortstate = self.list.GetSortState()
543 self.list.itemDataMap = {}
546 for key, subnet in vpn.subnets.items():
547 if self.list.GetItemCount() <= i:
548 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
550 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
551 self.list.SetStringItem(i, 1, str(subnet.weight))
552 self.list.SetStringItem(i, 2, subnet.owner)
553 self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
554 self.list.SetItemData(i, i)
557 while self.list.GetItemCount() > i:
558 self.list.DeleteItem(self.list.GetItemCount() - 1)
560 self.list.SortListItems(sortstate[0], sortstate[1])
563 class StatusPage(wx.Panel):
564 def __init__(self, parent, id):
565 wx.Panel.__init__(self, parent, id)
568 class GraphPage(wx.Window):
569 def __init__(self, parent, id):
570 wx.Window.__init__(self, parent, id)
573 class NetPage(wx.Notebook):
574 def __init__(self, parent, id):
575 wx.Notebook.__init__(self, parent)
576 self.settings = SettingsPage(self, id)
577 self.connections = ConnectionsPage(self, id)
578 self.nodes = NodesPage(self, id)
579 self.edges = EdgesPage(self, id)
580 self.subnets = SubnetsPage(self, id)
581 self.graph = GraphPage(self, id)
582 self.status = StatusPage(self, id)
584 self.AddPage(self.settings, 'Settings')
585 # self.AddPage(self.status, 'Status')
586 self.AddPage(self.connections, 'Connections')
587 self.AddPage(self.nodes, 'Nodes')
588 self.AddPage(self.edges, 'Edges')
589 self.AddPage(self.subnets, 'Subnets')
591 # self.AddPage(self.graph, 'Graph')
594 class MainWindow(wx.Frame):
595 def on_quit(self, event):
598 def on_timer(self, event):
600 self.np.nodes.refresh()
601 self.np.subnets.refresh()
602 self.np.edges.refresh()
603 self.np.connections.refresh()
605 def __init__(self, parent, id, title):
606 wx.Frame.__init__(self, parent, id, title)
608 menubar = wx.MenuBar()
611 menu.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
612 menubar.Append(menu, '&File')
614 # nb = wx.Notebook(self, -1)
615 # nb.SetPadding((0, 0))
616 self.np = NetPage(self, -1)
617 # nb.AddPage(np, 'VPN')
619 self.timer = wx.Timer(self, -1)
620 self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
621 self.timer.Start(1000)
622 self.Bind(wx.EVT_MENU, self.on_quit, id=1)
623 self.SetMenuBar(menubar)
628 mw = MainWindow(None, -1, 'Tinc GUI')
631 def OnTaskBarIcon(event):
636 icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
637 taskbaricon = wx.TaskBarIcon()
638 taskbaricon.SetIcon(icon, 'Tinc GUI')
639 wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)