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 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
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 confdir = '/etc/tinc'
133 f = open(self.pidfile)
134 info = string.split(f.readline())
137 # check if there is a UNIX socket as well
138 if self.pidfile.endswith('.pid'):
139 unixfile = self.pidfile.replace('.pid', '.socket');
141 unixfile = self.pidfile + '.socket';
143 if os.path.exists(unixfile):
144 # use it if it exists
145 print(unixfile + " exists!");
146 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
149 # otherwise connect via TCP
150 print(unixfile + " does not exist.");
155 s = socket.socket(af, socket.SOCK_STREAM)
156 s.connect((info[2], int(info[4])))
158 self.sf = s.makefile()
160 hello = string.split(self.sf.readline())
162 self.sf.write('0 ^' + info[1] + ' 17\r\n')
164 resp = string.split(self.sf.readline())
169 self.connections = {}
173 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
176 for node in self.nodes.values():
178 for edge in self.edges.values():
180 for subnet in self.subnets.values():
181 subnet.visited = False
182 for connections in self.connections.values():
183 connections.visited = False
186 resp = string.split(self.sf.readline())
194 node = self.nodes.get(resp[2]) or Node(resp[2:])
196 self.nodes[resp[2]] = node
200 edge = self.nodes.get((resp[2], resp[3])) or Edge(resp[2:])
202 self.edges[(resp[2], resp[3])] = edge
206 subnet = self.subnets.get((resp[2], resp[3])) or Subnet(resp[2:])
207 subnet.visited = True
208 self.subnets[(resp[2], resp[3])] = subnet
209 if subnet.owner == "(broadcast)":
211 self.nodes[subnet.owner].subnets[resp[2]] = subnet
215 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection(resp[2:])
216 connection.visited = True
217 self.connections[(resp[2], resp[3], resp[5])] = connection
221 for key, subnet in self.subnets.items():
222 if not subnet.visited:
223 del self.subnets[key]
225 for key, edge in self.edges.items():
229 for key, node in self.nodes.items():
233 for key, subnet in node.subnets.items():
234 if not subnet.visited:
235 del node.subnets[key]
237 for key, connection in self.connections.items():
238 if not connection.visited:
239 del self.connections[key]
244 def disconnect(self, name):
245 self.sf.write('18 12 ' + name + '\r\n')
247 resp = string.split(self.sf.readline())
249 def debug(self, level=-1):
250 self.sf.write('18 9 ' + str(level) + '\r\n')
252 resp = string.split(self.sf.readline())
255 def __init__(self, netname=None, pidfile=None):
256 if platform.system() == 'Windows':
257 sam = _winreg.KEY_READ
258 if platform.machine().endswith('64'):
259 sam = sam | _winreg.KEY_WOW64_64KEY
261 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
263 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
265 key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
266 VPN.confdir = _winreg.QueryValue(key, None)
271 self.netname = netname
272 self.confbase = os.path.join(VPN.confdir, netname)
274 self.confbase = VPN.confdir
276 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
278 if pidfile is not None:
279 self.pidfile = pidfile
281 if platform.system() == 'Windows':
282 self.pidfile = os.path.join(self.confbase, 'pid')
285 self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
287 self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
293 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
294 def __init__(self, parent, style):
295 wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
296 ListCtrlAutoWidthMixin.__init__(self)
297 ColumnSorterMixin.__init__(self, 16)
299 def GetListCtrl(self):
303 class SettingsPage(wx.Panel):
304 def on_debug_level(self, event):
305 vpn.debug(self.debug.GetValue())
307 def __init__(self, parent, id):
308 wx.Panel.__init__(self, parent, id)
309 grid = wx.FlexGridSizer(cols=2)
310 grid.AddGrowableCol(1, 1)
312 namelabel = wx.StaticText(self, -1, 'Name:')
313 self.name = wx.TextCtrl(self, -1, vpn.name)
315 grid.Add(self.name, 1, wx.EXPAND)
317 portlabel = wx.StaticText(self, -1, 'Port:')
318 self.port = wx.TextCtrl(self, -1, vpn.port)
322 debuglabel = wx.StaticText(self, -1, 'Debug level:')
323 self.debug = wx.SpinCtrl(self, min=0, max=5, initial=vpn.debug())
324 self.debug.Bind(wx.EVT_SPINCTRL, self.on_debug_level)
328 modelabel = wx.StaticText(self, -1, 'Mode:')
329 self.mode = wx.ComboBox(self, -1, style=wx.CB_READONLY, value='Router', choices=['Router', 'Switch', 'Hub'])
336 class ConnectionsPage(wx.Panel):
337 def __init__(self, parent, id):
338 wx.Panel.__init__(self, parent, id)
339 self.list = SuperListCtrl(self, id)
340 self.list.InsertColumn(0, 'Name')
341 self.list.InsertColumn(1, 'Address')
342 self.list.InsertColumn(2, 'Port')
343 self.list.InsertColumn(3, 'Options')
344 self.list.InsertColumn(4, 'Weight')
346 hbox = wx.BoxSizer(wx.HORIZONTAL)
347 hbox.Add(self.list, 1, wx.EXPAND)
351 class ContextMenu(wx.Menu):
352 def __init__(self, item):
353 wx.Menu.__init__(self)
357 disconnect = wx.MenuItem(self, -1, 'Disconnect')
358 self.AppendItem(disconnect)
359 self.Bind(wx.EVT_MENU, self.on_disconnect, id=disconnect.GetId())
361 def on_disconnect(self, event):
362 vpn.disconnect(self.item[0])
364 def on_context(self, event):
365 idx = event.GetIndex()
366 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
369 sortstate = self.list.GetSortState()
370 self.list.itemDataMap = {}
373 for key, connection in vpn.connections.items():
374 if self.list.GetItemCount() <= i:
375 self.list.InsertStringItem(i, connection.name)
377 self.list.SetStringItem(i, 0, connection.name)
378 self.list.SetStringItem(i, 1, connection.address)
379 self.list.SetStringItem(i, 2, connection.port)
380 self.list.SetStringItem(i, 3, str(connection.options))
381 self.list.SetStringItem(i, 4, str(connection.weight))
382 self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options,
384 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.on_context)
385 self.list.SetItemData(i, i)
388 while self.list.GetItemCount() > i:
389 self.list.DeleteItem(self.list.GetItemCount() - 1)
391 self.list.SortListItems(sortstate[0], sortstate[1])
394 class NodesPage(wx.Panel):
395 def __init__(self, parent, id):
396 wx.Panel.__init__(self, parent, id)
397 self.list = SuperListCtrl(self, id)
398 self.list.InsertColumn(0, 'Name')
399 self.list.InsertColumn(1, 'Address')
400 self.list.InsertColumn(2, 'Port')
401 self.list.InsertColumn(3, 'Cipher')
402 self.list.InsertColumn(4, 'Digest')
403 self.list.InsertColumn(5, 'MACLength')
404 self.list.InsertColumn(6, 'Compression')
405 self.list.InsertColumn(7, 'Options')
406 self.list.InsertColumn(8, 'Status')
407 self.list.InsertColumn(9, 'Nexthop')
408 self.list.InsertColumn(10, 'Via')
409 self.list.InsertColumn(11, 'Distance')
410 self.list.InsertColumn(12, 'PMTU')
411 self.list.InsertColumn(13, 'Min MTU')
412 self.list.InsertColumn(14, 'Max MTU')
413 self.list.InsertColumn(15, 'Since')
415 hbox = wx.BoxSizer(wx.HORIZONTAL)
416 hbox.Add(self.list, 1, wx.EXPAND)
421 sortstate = self.list.GetSortState()
422 self.list.itemDataMap = {}
425 for key, node in vpn.nodes.items():
426 if self.list.GetItemCount() <= i:
427 self.list.InsertStringItem(i, node.name)
429 self.list.SetStringItem(i, 0, node.name)
430 self.list.SetStringItem(i, 1, node.address)
431 self.list.SetStringItem(i, 2, node.port)
432 self.list.SetStringItem(i, 3, str(node.cipher))
433 self.list.SetStringItem(i, 4, str(node.digest))
434 self.list.SetStringItem(i, 5, str(node.maclength))
435 self.list.SetStringItem(i, 6, str(node.compression))
436 self.list.SetStringItem(i, 7, format(node.options, "x"))
437 self.list.SetStringItem(i, 8, format(node.status, "04x"))
438 self.list.SetStringItem(i, 9, node.nexthop)
439 self.list.SetStringItem(i, 10, node.via)
440 self.list.SetStringItem(i, 11, str(node.distance))
441 self.list.SetStringItem(i, 12, str(node.pmtu))
442 self.list.SetStringItem(i, 13, str(node.minmtu))
443 self.list.SetStringItem(i, 14, str(node.maxmtu))
444 if node.last_state_change:
445 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
448 self.list.SetStringItem(i, 15, since)
449 self.list.itemDataMap[i] = (node.name, node.address, node.port, node.cipher, node.digest, node.maclength,
450 node.compression, node.options, node.status, node.nexthop, node.via,
451 node.distance, node.pmtu, node.minmtu, node.maxmtu, since)
452 self.list.SetItemData(i, i)
455 while self.list.GetItemCount() > i:
456 self.list.DeleteItem(self.list.GetItemCount() - 1)
458 self.list.SortListItems(sortstate[0], sortstate[1])
461 class EdgesPage(wx.Panel):
462 def __init__(self, parent, id):
463 wx.Panel.__init__(self, parent, id)
464 self.list = SuperListCtrl(self, id)
465 self.list.InsertColumn(0, 'From')
466 self.list.InsertColumn(1, 'To')
467 self.list.InsertColumn(2, 'Address')
468 self.list.InsertColumn(3, 'Port')
469 self.list.InsertColumn(4, 'Options')
470 self.list.InsertColumn(5, 'Weight')
472 hbox = wx.BoxSizer(wx.HORIZONTAL)
473 hbox.Add(self.list, 1, wx.EXPAND)
478 sortstate = self.list.GetSortState()
479 self.list.itemDataMap = {}
482 for key, edge in vpn.edges.items():
483 if self.list.GetItemCount() <= i:
484 self.list.InsertStringItem(i, edge.source)
486 self.list.SetStringItem(i, 0, edge.source)
487 self.list.SetStringItem(i, 1, edge.sink)
488 self.list.SetStringItem(i, 2, edge.address)
489 self.list.SetStringItem(i, 3, edge.port)
490 self.list.SetStringItem(i, 4, format(edge.options, "x"))
491 self.list.SetStringItem(i, 5, str(edge.weight))
492 self.list.itemDataMap[i] = (edge.source, edge.sink, edge.address, edge.port, edge.options, edge.weight)
493 self.list.SetItemData(i, i)
496 while self.list.GetItemCount() > i:
497 self.list.DeleteItem(self.list.GetItemCount() - 1)
499 self.list.SortListItems(sortstate[0], sortstate[1])
502 class SubnetsPage(wx.Panel):
503 def __init__(self, parent, id):
504 wx.Panel.__init__(self, parent, id)
505 self.list = SuperListCtrl(self, id)
506 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
507 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
508 self.list.InsertColumn(2, 'Owner')
509 hbox = wx.BoxSizer(wx.HORIZONTAL)
510 hbox.Add(self.list, 1, wx.EXPAND)
515 sortstate = self.list.GetSortState()
516 self.list.itemDataMap = {}
519 for key, subnet in vpn.subnets.items():
520 if self.list.GetItemCount() <= i:
521 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
523 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
524 self.list.SetStringItem(i, 1, str(subnet.weight))
525 self.list.SetStringItem(i, 2, subnet.owner)
526 self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
527 self.list.SetItemData(i, i)
530 while self.list.GetItemCount() > i:
531 self.list.DeleteItem(self.list.GetItemCount() - 1)
533 self.list.SortListItems(sortstate[0], sortstate[1])
536 class StatusPage(wx.Panel):
537 def __init__(self, parent, id):
538 wx.Panel.__init__(self, parent, id)
541 class GraphPage(wx.Window):
542 def __init__(self, parent, id):
543 wx.Window.__init__(self, parent, id)
546 class NetPage(wx.Notebook):
547 def __init__(self, parent, id):
548 wx.Notebook.__init__(self, parent)
549 self.settings = SettingsPage(self, id)
550 self.connections = ConnectionsPage(self, id)
551 self.nodes = NodesPage(self, id)
552 self.edges = EdgesPage(self, id)
553 self.subnets = SubnetsPage(self, id)
554 self.graph = GraphPage(self, id)
555 self.status = StatusPage(self, id)
557 self.AddPage(self.settings, 'Settings')
558 # self.AddPage(self.status, 'Status')
559 self.AddPage(self.connections, 'Connections')
560 self.AddPage(self.nodes, 'Nodes')
561 self.AddPage(self.edges, 'Edges')
562 self.AddPage(self.subnets, 'Subnets')
564 # self.AddPage(self.graph, 'Graph')
567 class MainWindow(wx.Frame):
568 def __init__(self, parent, id, title):
569 wx.Frame.__init__(self, parent, id, title)
571 menubar = wx.MenuBar()
574 menu.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
575 menubar.Append(menu, '&File')
577 # nb = wx.Notebook(self, -1)
578 # nb.SetPadding((0, 0))
579 self.np = NetPage(self, -1)
580 # nb.AddPage(np, 'VPN')
582 self.timer = wx.Timer(self, -1)
583 self.Bind(wx.EVT_TIMER, self.on_timer, self.timer)
584 self.timer.Start(1000)
585 self.Bind(wx.EVT_MENU, self.on_quit, id=1)
586 self.SetMenuBar(menubar)
589 def on_quit(self, event):
592 def on_timer(self, event):
594 self.np.nodes.refresh()
595 self.np.subnets.refresh()
596 self.np.edges.refresh()
597 self.np.connections.refresh()
600 def main(netname, pidfile):
604 netname = os.getenv('NETNAME')
606 vpn = VPN(netname, pidfile)
610 mw = MainWindow(None, -1, 'Tinc GUI')
613 def OnTaskBarIcon(event):
618 icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
619 taskbaricon = wx.TaskBarIcon()
620 taskbaricon.SetIcon(icon, 'Tinc GUI')
621 wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)
628 if __name__ == '__main__':
629 argparser = ArgumentParser(epilog='Report bugs to tinc@tinc-vpn.org.')
631 argparser.add_argument('-n', '--net', metavar='NETNAME', dest='netname', help='Connect to net NETNAME')
632 argparser.add_argument('-p', '--pidfile', help='Path to the pid file (containing the controlcookie)')
634 options = argparser.parse_args()
636 main(options.netname, options.pidfile)