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'
115 f = open(self.pidfile)
116 info = string.split(f.readline())
119 # check if there is a UNIX socket as well
120 if self.pidfile.endswith(".pid"):
121 unixfile = self.pidfile.replace(".pid", ".socket");
123 unixfile = self.pidfile + ".socket";
125 if os.path.exists(unixfile):
126 # use it if it exists
127 print(unixfile + " exists!");
128 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
131 # otherwise connect via TCP
132 print(unixfile + " does not exist.");
133 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
134 s.connect((info[2], int(info[4])))
136 self.sf = s.makefile()
138 hello = string.split(self.sf.readline())
140 self.sf.write('0 ^' + info[1] + ' 17\r\n')
142 resp = string.split(self.sf.readline())
147 self.connections = {}
151 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
154 for node in self.nodes.values():
156 for edge in self.edges.values():
158 for subnet in self.subnets.values():
159 subnet.visited = False
160 for connections in self.connections.values():
161 connections.visited = False
164 resp = string.split(self.sf.readline())
172 node = self.nodes.get(resp[2]) or Node()
175 self.nodes[resp[2]] = node
179 edge = self.nodes.get((resp[2], resp[3])) or Edge()
182 self.edges[(resp[2], resp[3])] = edge
186 subnet = self.subnets.get((resp[2], resp[3])) or Subnet()
187 subnet.parse(resp[2:])
188 subnet.visited = True
189 self.subnets[(resp[2], resp[3])] = subnet
190 self.nodes[subnet.owner].subnets[resp[2]] = subnet
194 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection()
195 connection.parse(resp[2:])
196 connection.visited = True
197 self.connections[(resp[2], resp[3], resp[5])] = connection
201 for key, subnet in self.subnets.items():
202 if not subnet.visited:
203 del self.subnets[key]
205 for key, edge in self.edges.items():
209 for key, node in self.nodes.items():
213 for key, subnet in node.subnets.items():
214 if not subnet.visited:
215 del node.subnets[key]
217 for key, connection in self.connections.items():
218 if not connection.visited:
219 del self.connections[key]
224 def disconnect(self, name):
225 self.sf.write('18 12 ' + name + '\r\n')
227 resp = string.split(self.sf.readline())
229 def debug(self, level = -1):
230 self.sf.write('18 9 ' + str(level) + '\r\n')
232 resp = string.split(self.sf.readline())
235 def __init__(self, netname = None, pidfile = None):
236 if platform.system() == 'Windows':
237 sam = _winreg.KEY_READ
238 if platform.machine().endswith('64'):
239 sam = sam | _winreg.KEY_WOW64_64KEY
241 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
243 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
245 key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
246 VPN.confdir = _winreg.QueryValue(key, None)
251 self.netname = netname
252 self.confbase = os.path.join(VPN.confdir, netname)
254 self.confbase = VPN.confdir
256 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
259 self.pidfile = pidfile
261 if platform.system() == 'Windows':
262 self.pidfile = os.path.join(self.confbase, 'pid')
265 self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
267 self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
276 def usage(exitcode = 0):
277 print('Usage: ' + argv0 + ' [options]')
278 print('\nValid options are:')
279 print(' -n, --net=NETNAME Connect to net NETNAME.')
280 print(' --pidfile=FILENAME Read control cookie from FILENAME.')
281 print(' --help Display this help and exit.')
282 print('\nReport bugs to tinc@tinc-vpn.org.')
286 if sys.argv[0] in ('-n', '--net'):
288 netname = sys.argv[0]
289 elif sys.argv[0] in ('--pidfile'):
291 pidfile = sys.argv[0]
292 elif sys.argv[0] in ('--help'):
295 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
301 netname = os.getenv("NETNAME")
306 vpn = VPN(netname, pidfile)
309 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
310 def __init__(self, parent, style):
311 wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
312 ListCtrlAutoWidthMixin.__init__(self)
313 ColumnSorterMixin.__init__(self, 16)
315 def GetListCtrl(self):
319 class SettingsPage(wx.Panel):
320 def OnDebugLevel(self, event):
321 vpn.debug(self.debug.GetValue())
323 def __init__(self, parent, id):
324 wx.Panel.__init__(self, parent, id)
325 grid = wx.FlexGridSizer(cols = 2)
326 grid.AddGrowableCol(1, 1)
328 namelabel = wx.StaticText(self, -1, 'Name:')
329 self.name = wx.TextCtrl(self, -1, vpn.name)
331 grid.Add(self.name, 1, wx.EXPAND)
333 portlabel = wx.StaticText(self, -1, 'Port:')
334 self.port = wx.TextCtrl(self, -1, vpn.port)
338 debuglabel = wx.StaticText(self, -1, 'Debug level:')
339 self.debug = wx.SpinCtrl(self, min = 0, max = 5, initial = vpn.debug())
340 self.debug.Bind(wx.EVT_SPINCTRL, self.OnDebugLevel)
344 modelabel = wx.StaticText(self, -1, 'Mode:')
345 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
351 class ConnectionsPage(wx.Panel):
352 def __init__(self, parent, id):
353 wx.Panel.__init__(self, parent, id)
354 self.list = SuperListCtrl(self, id)
355 self.list.InsertColumn(0, 'Name')
356 self.list.InsertColumn(1, 'Address')
357 self.list.InsertColumn(2, 'Port')
358 self.list.InsertColumn(3, 'Options')
359 self.list.InsertColumn(4, 'Weight')
361 hbox = wx.BoxSizer(wx.HORIZONTAL)
362 hbox.Add(self.list, 1, wx.EXPAND)
366 class ContextMenu(wx.Menu):
367 def __init__(self, item):
368 wx.Menu.__init__(self)
372 disconnect = wx.MenuItem(self, -1, 'Disconnect')
373 self.AppendItem(disconnect)
374 self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
376 def OnDisconnect(self, event):
377 vpn.disconnect(self.item[0])
379 def OnContext(self, event):
381 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
384 sortstate = self.list.GetSortState()
385 self.list.itemDataMap = {}
388 for key, connection in vpn.connections.items():
389 if self.list.GetItemCount() <= i:
390 self.list.InsertStringItem(i, connection.name)
392 self.list.SetStringItem(i, 0, connection.name)
393 self.list.SetStringItem(i, 1, connection.address)
394 self.list.SetStringItem(i, 2, connection.port)
395 self.list.SetStringItem(i, 3, str(connection.options))
396 self.list.SetStringItem(i, 4, str(connection.weight))
397 self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options, connection.weight)
398 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnContext)
399 self.list.SetItemData(i, i)
402 while self.list.GetItemCount() > i:
403 self.list.DeleteItem(self.list.GetItemCount() - 1)
405 self.list.SortListItems(sortstate[0], sortstate[1])
407 class NodesPage(wx.Panel):
408 def __init__(self, parent, id):
409 wx.Panel.__init__(self, parent, id)
410 self.list = SuperListCtrl(self, id)
411 self.list.InsertColumn( 0, 'Name')
412 self.list.InsertColumn( 1, 'Address')
413 self.list.InsertColumn( 2, 'Port')
414 self.list.InsertColumn( 3, 'Cipher')
415 self.list.InsertColumn( 4, 'Digest')
416 self.list.InsertColumn( 5, 'MACLength')
417 self.list.InsertColumn( 6, 'Compression')
418 self.list.InsertColumn( 7, 'Options')
419 self.list.InsertColumn( 8, 'Status')
420 self.list.InsertColumn( 9, 'Nexthop')
421 self.list.InsertColumn(10, 'Via')
422 self.list.InsertColumn(11, 'Distance')
423 self.list.InsertColumn(12, 'PMTU')
424 self.list.InsertColumn(13, 'Min MTU')
425 self.list.InsertColumn(14, 'Max MTU')
426 self.list.InsertColumn(15, 'Since')
428 hbox = wx.BoxSizer(wx.HORIZONTAL)
429 hbox.Add(self.list, 1, wx.EXPAND)
434 sortstate = self.list.GetSortState()
435 self.list.itemDataMap = {}
438 for key, node in vpn.nodes.items():
439 if self.list.GetItemCount() <= i:
440 self.list.InsertStringItem(i, node.name)
442 self.list.SetStringItem(i, 0, node.name)
443 self.list.SetStringItem(i, 1, node.address)
444 self.list.SetStringItem(i, 2, node.port)
445 self.list.SetStringItem(i, 3, str(node.cipher))
446 self.list.SetStringItem(i, 4, str(node.digest))
447 self.list.SetStringItem(i, 5, str(node.maclength))
448 self.list.SetStringItem(i, 6, str(node.compression))
449 self.list.SetStringItem(i, 7, format(node.options, "x"))
450 self.list.SetStringItem(i, 8, format(node.status, "04x"))
451 self.list.SetStringItem(i, 9, node.nexthop)
452 self.list.SetStringItem(i, 10, node.via)
453 self.list.SetStringItem(i, 11, str(node.distance))
454 self.list.SetStringItem(i, 12, str(node.pmtu))
455 self.list.SetStringItem(i, 13, str(node.minmtu))
456 self.list.SetStringItem(i, 14, str(node.maxmtu))
457 if node.last_state_change:
458 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
461 self.list.SetStringItem(i, 15, since)
462 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)
463 self.list.SetItemData(i, i)
466 while self.list.GetItemCount() > i:
467 self.list.DeleteItem(self.list.GetItemCount() - 1)
469 self.list.SortListItems(sortstate[0], sortstate[1])
471 class EdgesPage(wx.Panel):
472 def __init__(self, parent, id):
473 wx.Panel.__init__(self, parent, id)
474 self.list = SuperListCtrl(self, id)
475 self.list.InsertColumn(0, 'From')
476 self.list.InsertColumn(1, 'To')
477 self.list.InsertColumn(2, 'Address')
478 self.list.InsertColumn(3, 'Port')
479 self.list.InsertColumn(4, 'Options')
480 self.list.InsertColumn(5, 'Weight')
482 hbox = wx.BoxSizer(wx.HORIZONTAL)
483 hbox.Add(self.list, 1, wx.EXPAND)
488 sortstate = self.list.GetSortState()
489 self.list.itemDataMap = {}
492 for key, edge in vpn.edges.items():
493 if self.list.GetItemCount() <= i:
494 self.list.InsertStringItem(i, edge.fr)
496 self.list.SetStringItem(i, 0, edge.fr)
497 self.list.SetStringItem(i, 1, edge.to)
498 self.list.SetStringItem(i, 2, edge.address)
499 self.list.SetStringItem(i, 3, edge.port)
500 self.list.SetStringItem(i, 4, format(edge.options, "x"))
501 self.list.SetStringItem(i, 5, str(edge.weight))
502 self.list.itemDataMap[i] = (edge.fr, edge.to, edge.address, edge.port, edge.options, edge.weight)
503 self.list.SetItemData(i, i)
506 while self.list.GetItemCount() > i:
507 self.list.DeleteItem(self.list.GetItemCount() - 1)
509 self.list.SortListItems(sortstate[0], sortstate[1])
511 class SubnetsPage(wx.Panel):
512 def __init__(self, parent, id):
513 wx.Panel.__init__(self, parent, id)
514 self.list = SuperListCtrl(self, id)
515 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
516 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
517 self.list.InsertColumn(2, 'Owner')
518 hbox = wx.BoxSizer(wx.HORIZONTAL)
519 hbox.Add(self.list, 1, wx.EXPAND)
524 sortstate = self.list.GetSortState()
525 self.list.itemDataMap = {}
528 for key, subnet in vpn.subnets.items():
529 if self.list.GetItemCount() <= i:
530 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
532 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
533 self.list.SetStringItem(i, 1, subnet.weight)
534 self.list.SetStringItem(i, 2, subnet.owner)
535 self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
536 self.list.SetItemData(i, i)
539 while self.list.GetItemCount() > i:
540 self.list.DeleteItem(self.list.GetItemCount() - 1)
542 self.list.SortListItems(sortstate[0], sortstate[1])
544 class StatusPage(wx.Panel):
545 def __init__(self, parent, id):
546 wx.Panel.__init__(self, parent, id)
548 class GraphPage(wx.Window):
549 def __init__(self, parent, id):
550 wx.Window.__init__(self, parent, id)
552 class NetPage(wx.Notebook):
553 def __init__(self, parent, id):
554 wx.Notebook.__init__(self, parent)
555 self.settings = SettingsPage(self, id)
556 self.connections = ConnectionsPage(self, id)
557 self.nodes = NodesPage(self, id)
558 self.edges = EdgesPage(self, id)
559 self.subnets = SubnetsPage(self, id)
560 self.graph = GraphPage(self, id)
561 self.status = StatusPage(self, id)
563 self.AddPage(self.settings, 'Settings')
564 #self.AddPage(self.status, 'Status')
565 self.AddPage(self.connections, 'Connections')
566 self.AddPage(self.nodes, 'Nodes')
567 self.AddPage(self.edges, 'Edges')
568 self.AddPage(self.subnets, 'Subnets')
569 #self.AddPage(self.graph, 'Graph')
572 class MainWindow(wx.Frame):
573 def OnQuit(self, event):
576 def OnTimer(self, event):
578 self.np.nodes.refresh()
579 self.np.subnets.refresh()
580 self.np.edges.refresh()
581 self.np.connections.refresh()
583 def __init__(self, parent, id, title):
584 wx.Frame.__init__(self, parent, id, title)
586 menubar = wx.MenuBar()
588 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
589 menubar.Append(file, '&File')
591 #nb = wx.Notebook(self, -1)
592 #nb.SetPadding((0, 0))
593 self.np = NetPage(self, -1)
594 #nb.AddPage(np, 'VPN')
596 self.timer = wx.Timer(self, -1)
597 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
598 self.timer.Start(1000)
599 self.Bind(wx.EVT_MENU, self.OnQuit, id=1)
600 self.SetMenuBar(menubar)
604 mw = MainWindow(None, -1, 'Tinc GUI')
606 #def OnTaskBarIcon(event):
609 #icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
610 #taskbaricon = wx.TaskBarIcon()
611 #taskbaricon.SetIcon(icon, 'Tinc GUI')
612 #wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)