3 # tinc-gui -- GUI for controlling a running tincd
4 # Copyright (C) 2009-2012 Guus Sliepen <guus@tinc-vpn.org>
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
27 from wx.lib.mixins.listctrl import ColumnSorterMixin
28 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
30 if platform.system == 'Windows':
33 # Classes to interface with a running tinc daemon
41 REQ_DUMP_CONNECTIONS = 6
54 def parse(self, args):
56 self.address = args[1]
58 self.cipher = int(args[4])
59 self.digest = int(args[5])
60 self.maclength = int(args[6])
61 self.compression = int(args[7])
62 self.options = int(args[8], 0x10)
63 self.status = int(args[9], 0x10)
64 self.nexthop = args[10]
66 self.distance = int(args[12])
67 self.pmtu = int(args[13])
68 self.minmtu = int(args[14])
69 self.maxmtu = int(args[15])
70 self.last_state_change = float(args[16])
75 def parse(self, args):
78 self.address = args[2]
80 self.options = int(args[5], 16)
81 self.weight = int(args[6])
84 def parse(self, args):
85 if args[0].find('#') >= 0:
86 (address, self.weight) = args[0].split('#', 1)
91 if address.find('/') >= 0:
92 (self.address, self.prefixlen) = address.split('/', 1)
94 self.address = address
100 def parse(self, args):
102 self.address = args[1]
104 self.options = int(args[4], 0x10)
105 self.socket = int(args[5])
106 self.status = int(args[6], 0x10)
110 confdir = '/etc/tinc'
114 f = open(self.pidfile)
115 info = string.split(f.readline())
117 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
118 s.connect((info[2], int(info[4])))
119 self.sf = s.makefile()
121 hello = string.split(self.sf.readline())
123 self.sf.write('0 ^' + info[1] + ' 17\r\n')
125 resp = string.split(self.sf.readline())
130 self.connections = {}
134 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
137 for node in self.nodes.values():
139 for edge in self.edges.values():
141 for subnet in self.subnets.values():
142 subnet.visited = False
143 for connections in self.connections.values():
144 connections.visited = False
147 resp = string.split(self.sf.readline())
155 node = self.nodes.get(resp[2]) or Node()
158 self.nodes[resp[2]] = node
162 edge = self.nodes.get((resp[2], resp[3])) or Edge()
165 self.edges[(resp[2], resp[3])] = edge
169 subnet = self.subnets.get((resp[2], resp[3])) or Subnet()
170 subnet.parse(resp[2:])
171 subnet.visited = True
172 self.subnets[(resp[2], resp[3])] = subnet
173 self.nodes[subnet.owner].subnets[resp[2]] = subnet
177 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection()
178 connection.parse(resp[2:])
179 connection.visited = True
180 self.connections[(resp[2], resp[3], resp[5])] = connection
184 for key, subnet in self.subnets.items():
185 if not subnet.visited:
186 del self.subnets[key]
188 for key, edge in self.edges.items():
192 for key, node in self.nodes.items():
196 for key, subnet in node.subnets.items():
197 if not subnet.visited:
198 del node.subnets[key]
200 for key, connection in self.connections.items():
201 if not connection.visited:
202 del self.connections[key]
207 def disconnect(self, name):
208 self.sf.write('18 12 ' + name + '\r\n')
210 resp = string.split(self.sf.readline())
212 def debug(self, level = -1):
213 self.sf.write('18 9 ' + str(level) + '\r\n')
215 resp = string.split(self.sf.readline())
218 def __init__(self, netname = None, pidfile = None):
219 if platform.system == 'Windows':
221 reg = _winreg.ConnectRegistry(None, HKEY_LOCAL_MACHINE)
222 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc")
223 VPN.confdir = _winreg.QueryValue(key, None)
228 self.netname = netname
229 self.confbase = os.path.join(VPN.confdir, netname)
231 self.confbase = VPN.confdir
233 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
236 self.pidfile = pidfile
238 if platform.system == 'Windows':
239 self.pidfile = os.path.join(self.confbase, 'pid')
242 self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
244 self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
253 def usage(exitcode = 0):
254 print('Usage: ' + argv0 + ' [options]')
255 print('\nValid options are:')
256 print(' -n, --net=NETNAME Connect to net NETNAME.')
257 print(' --pidfile=FILENAME Read control cookie from FILENAME.')
258 print(' --help Display this help and exit.')
259 print('\nReport bugs to tinc@tinc-vpn.org.')
263 if sys.argv[0] in ('-n', '--net'):
265 netname = sys.argv[0]
266 elif sys.argv[0] in ('--pidfile'):
268 pidfile = sys.argv[0]
269 elif sys.argv[0] in ('--help'):
272 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
278 netname = os.getenv("NETNAME")
283 vpn = VPN(netname, pidfile)
286 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
287 def __init__(self, parent, style):
288 wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
289 ListCtrlAutoWidthMixin.__init__(self)
290 ColumnSorterMixin.__init__(self, 16)
292 def GetListCtrl(self):
296 class SettingsPage(wx.Panel):
297 def OnDebugLevel(self, event):
298 vpn.debug(self.debug.GetValue())
300 def __init__(self, parent, id):
301 wx.Panel.__init__(self, parent, id)
302 grid = wx.FlexGridSizer(cols = 2)
303 grid.AddGrowableCol(1, 1)
305 namelabel = wx.StaticText(self, -1, 'Name:')
306 self.name = wx.TextCtrl(self, -1, vpn.name)
308 grid.Add(self.name, 1, wx.EXPAND)
310 portlabel = wx.StaticText(self, -1, 'Port:')
311 self.port = wx.TextCtrl(self, -1, vpn.port)
315 debuglabel = wx.StaticText(self, -1, 'Debug level:')
316 self.debug = wx.SpinCtrl(self, min = 0, max = 5, initial = vpn.debug())
317 self.debug.Bind(wx.EVT_SPINCTRL, self.OnDebugLevel)
321 modelabel = wx.StaticText(self, -1, 'Mode:')
322 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
328 class ConnectionsPage(wx.Panel):
329 def __init__(self, parent, id):
330 wx.Panel.__init__(self, parent, id)
331 self.list = SuperListCtrl(self, id)
332 self.list.InsertColumn(0, 'Name')
333 self.list.InsertColumn(1, 'Address')
334 self.list.InsertColumn(2, 'Port')
335 self.list.InsertColumn(3, 'Options')
336 self.list.InsertColumn(4, 'Weight')
338 hbox = wx.BoxSizer(wx.HORIZONTAL)
339 hbox.Add(self.list, 1, wx.EXPAND)
343 class ContextMenu(wx.Menu):
344 def __init__(self, item):
345 wx.Menu.__init__(self)
349 disconnect = wx.MenuItem(self, -1, 'Disconnect')
350 self.AppendItem(disconnect)
351 self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
353 def OnDisconnect(self, event):
354 vpn.disconnect(self.item[0])
356 def OnContext(self, event):
358 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
361 sortstate = self.list.GetSortState()
362 self.list.itemDataMap = {}
365 for key, connection in vpn.connections.items():
366 if self.list.GetItemCount() <= i:
367 self.list.InsertStringItem(i, connection.name)
369 self.list.SetStringItem(i, 0, connection.name)
370 self.list.SetStringItem(i, 1, connection.address)
371 self.list.SetStringItem(i, 2, connection.port)
372 self.list.SetStringItem(i, 3, str(connection.options))
373 self.list.SetStringItem(i, 4, str(connection.weight))
374 self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options, connection.weight)
375 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnContext)
376 self.list.SetItemData(i, i)
379 while self.list.GetItemCount() > i:
380 self.list.DeleteItem(self.list.GetItemCount() - 1)
382 self.list.SortListItems(sortstate[0], sortstate[1])
384 class NodesPage(wx.Panel):
385 def __init__(self, parent, id):
386 wx.Panel.__init__(self, parent, id)
387 self.list = SuperListCtrl(self, id)
388 self.list.InsertColumn( 0, 'Name')
389 self.list.InsertColumn( 1, 'Address')
390 self.list.InsertColumn( 2, 'Port')
391 self.list.InsertColumn( 3, 'Cipher')
392 self.list.InsertColumn( 4, 'Digest')
393 self.list.InsertColumn( 5, 'MACLength')
394 self.list.InsertColumn( 6, 'Compression')
395 self.list.InsertColumn( 7, 'Options')
396 self.list.InsertColumn( 8, 'Status')
397 self.list.InsertColumn( 9, 'Nexthop')
398 self.list.InsertColumn(10, 'Via')
399 self.list.InsertColumn(11, 'Distance')
400 self.list.InsertColumn(12, 'PMTU')
401 self.list.InsertColumn(13, 'Min MTU')
402 self.list.InsertColumn(14, 'Max MTU')
403 self.list.InsertColumn(15, 'Since')
405 hbox = wx.BoxSizer(wx.HORIZONTAL)
406 hbox.Add(self.list, 1, wx.EXPAND)
411 sortstate = self.list.GetSortState()
412 self.list.itemDataMap = {}
415 for key, node in vpn.nodes.items():
416 if self.list.GetItemCount() <= i:
417 self.list.InsertStringItem(i, node.name)
419 self.list.SetStringItem(i, 0, node.name)
420 self.list.SetStringItem(i, 1, node.address)
421 self.list.SetStringItem(i, 2, node.port)
422 self.list.SetStringItem(i, 3, str(node.cipher))
423 self.list.SetStringItem(i, 4, str(node.digest))
424 self.list.SetStringItem(i, 5, str(node.maclength))
425 self.list.SetStringItem(i, 6, str(node.compression))
426 self.list.SetStringItem(i, 7, format(node.options, "x"))
427 self.list.SetStringItem(i, 8, format(node.status, "04x"))
428 self.list.SetStringItem(i, 9, node.nexthop)
429 self.list.SetStringItem(i, 10, node.via)
430 self.list.SetStringItem(i, 11, str(node.distance))
431 self.list.SetStringItem(i, 12, str(node.pmtu))
432 self.list.SetStringItem(i, 13, str(node.minmtu))
433 self.list.SetStringItem(i, 14, str(node.maxmtu))
434 if node.last_state_change:
435 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
438 self.list.SetStringItem(i, 15, since)
439 self.list.itemDataMap[i] = (node.name, node.address, node.port, node.cipher, node.digest, node.maclength, node.compression, node.options, node.status, node.nexthop, node.via, node.distance, node.pmtu, node.minmtu, node.maxmtu, since)
440 self.list.SetItemData(i, i)
443 while self.list.GetItemCount() > i:
444 self.list.DeleteItem(self.list.GetItemCount() - 1)
446 self.list.SortListItems(sortstate[0], sortstate[1])
448 class EdgesPage(wx.Panel):
449 def __init__(self, parent, id):
450 wx.Panel.__init__(self, parent, id)
451 self.list = SuperListCtrl(self, id)
452 self.list.InsertColumn(0, 'From')
453 self.list.InsertColumn(1, 'To')
454 self.list.InsertColumn(2, 'Address')
455 self.list.InsertColumn(3, 'Port')
456 self.list.InsertColumn(4, 'Options')
457 self.list.InsertColumn(5, 'Weight')
459 hbox = wx.BoxSizer(wx.HORIZONTAL)
460 hbox.Add(self.list, 1, wx.EXPAND)
465 sortstate = self.list.GetSortState()
466 self.list.itemDataMap = {}
469 for key, edge in vpn.edges.items():
470 if self.list.GetItemCount() <= i:
471 self.list.InsertStringItem(i, edge.fr)
473 self.list.SetStringItem(i, 0, edge.fr)
474 self.list.SetStringItem(i, 1, edge.to)
475 self.list.SetStringItem(i, 2, edge.address)
476 self.list.SetStringItem(i, 3, edge.port)
477 self.list.SetStringItem(i, 4, format(edge.options, "x"))
478 self.list.SetStringItem(i, 5, str(edge.weight))
479 self.list.itemDataMap[i] = (edge.fr, edge.to, edge.address, edge.port, edge.options, edge.weight)
480 self.list.SetItemData(i, i)
483 while self.list.GetItemCount() > i:
484 self.list.DeleteItem(self.list.GetItemCount() - 1)
486 self.list.SortListItems(sortstate[0], sortstate[1])
488 class SubnetsPage(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, 'Subnet', wx.LIST_FORMAT_RIGHT)
493 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
494 self.list.InsertColumn(2, 'Owner')
495 hbox = wx.BoxSizer(wx.HORIZONTAL)
496 hbox.Add(self.list, 1, wx.EXPAND)
501 sortstate = self.list.GetSortState()
502 self.list.itemDataMap = {}
505 for key, subnet in vpn.subnets.items():
506 if self.list.GetItemCount() <= i:
507 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
509 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
510 self.list.SetStringItem(i, 1, subnet.weight)
511 self.list.SetStringItem(i, 2, subnet.owner)
512 self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
513 self.list.SetItemData(i, i)
516 while self.list.GetItemCount() > i:
517 self.list.DeleteItem(self.list.GetItemCount() - 1)
519 self.list.SortListItems(sortstate[0], sortstate[1])
521 class StatusPage(wx.Panel):
522 def __init__(self, parent, id):
523 wx.Panel.__init__(self, parent, id)
525 class GraphPage(wx.Window):
526 def __init__(self, parent, id):
527 wx.Window.__init__(self, parent, id)
529 class NetPage(wx.Notebook):
530 def __init__(self, parent, id):
531 wx.Notebook.__init__(self, parent)
532 self.settings = SettingsPage(self, id)
533 self.connections = ConnectionsPage(self, id)
534 self.nodes = NodesPage(self, id)
535 self.edges = EdgesPage(self, id)
536 self.subnets = SubnetsPage(self, id)
537 self.graph = GraphPage(self, id)
538 self.status = StatusPage(self, id)
540 self.AddPage(self.settings, 'Settings')
541 #self.AddPage(self.status, 'Status')
542 self.AddPage(self.connections, 'Connections')
543 self.AddPage(self.nodes, 'Nodes')
544 self.AddPage(self.edges, 'Edges')
545 self.AddPage(self.subnets, 'Subnets')
546 #self.AddPage(self.graph, 'Graph')
549 class MainWindow(wx.Frame):
550 def OnQuit(self, event):
553 def OnTimer(self, event):
555 self.np.nodes.refresh()
556 self.np.subnets.refresh()
557 self.np.edges.refresh()
558 self.np.connections.refresh()
560 def __init__(self, parent, id, title):
561 wx.Frame.__init__(self, parent, id, title)
563 menubar = wx.MenuBar()
565 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
566 menubar.Append(file, '&File')
568 #nb = wx.Notebook(self, -1)
569 #nb.SetPadding((0, 0))
570 self.np = NetPage(self, -1)
571 #nb.AddPage(np, 'VPN')
573 self.timer = wx.Timer(self, -1)
574 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
575 self.timer.Start(1000)
576 self.Bind(wx.EVT_MENU, self.OnQuit, id=1)
577 self.SetMenuBar(menubar)
581 mw = MainWindow(None, -1, 'Tinc GUI')
583 #def OnTaskBarIcon(event):
586 #icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
587 #taskbaricon = wx.TaskBarIcon()
588 #taskbaricon.SetIcon(icon, 'Tinc GUI')
589 #wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)