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':
238 reg = _winreg.ConnectRegistry(None, HKEY_LOCAL_MACHINE)
239 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc")
240 VPN.confdir = _winreg.QueryValue(key, None)
245 self.netname = netname
246 self.confbase = os.path.join(VPN.confdir, netname)
248 self.confbase = VPN.confdir
250 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
253 self.pidfile = pidfile
255 if platform.system == 'Windows':
256 self.pidfile = os.path.join(self.confbase, 'pid')
259 self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
261 self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
270 def usage(exitcode = 0):
271 print('Usage: ' + argv0 + ' [options]')
272 print('\nValid options are:')
273 print(' -n, --net=NETNAME Connect to net NETNAME.')
274 print(' --pidfile=FILENAME Read control cookie from FILENAME.')
275 print(' --help Display this help and exit.')
276 print('\nReport bugs to tinc@tinc-vpn.org.')
280 if sys.argv[0] in ('-n', '--net'):
282 netname = sys.argv[0]
283 elif sys.argv[0] in ('--pidfile'):
285 pidfile = sys.argv[0]
286 elif sys.argv[0] in ('--help'):
289 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
295 netname = os.getenv("NETNAME")
300 vpn = VPN(netname, pidfile)
303 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
304 def __init__(self, parent, style):
305 wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
306 ListCtrlAutoWidthMixin.__init__(self)
307 ColumnSorterMixin.__init__(self, 16)
309 def GetListCtrl(self):
313 class SettingsPage(wx.Panel):
314 def OnDebugLevel(self, event):
315 vpn.debug(self.debug.GetValue())
317 def __init__(self, parent, id):
318 wx.Panel.__init__(self, parent, id)
319 grid = wx.FlexGridSizer(cols = 2)
320 grid.AddGrowableCol(1, 1)
322 namelabel = wx.StaticText(self, -1, 'Name:')
323 self.name = wx.TextCtrl(self, -1, vpn.name)
325 grid.Add(self.name, 1, wx.EXPAND)
327 portlabel = wx.StaticText(self, -1, 'Port:')
328 self.port = wx.TextCtrl(self, -1, vpn.port)
332 debuglabel = wx.StaticText(self, -1, 'Debug level:')
333 self.debug = wx.SpinCtrl(self, min = 0, max = 5, initial = vpn.debug())
334 self.debug.Bind(wx.EVT_SPINCTRL, self.OnDebugLevel)
338 modelabel = wx.StaticText(self, -1, 'Mode:')
339 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
345 class ConnectionsPage(wx.Panel):
346 def __init__(self, parent, id):
347 wx.Panel.__init__(self, parent, id)
348 self.list = SuperListCtrl(self, id)
349 self.list.InsertColumn(0, 'Name')
350 self.list.InsertColumn(1, 'Address')
351 self.list.InsertColumn(2, 'Port')
352 self.list.InsertColumn(3, 'Options')
353 self.list.InsertColumn(4, 'Weight')
355 hbox = wx.BoxSizer(wx.HORIZONTAL)
356 hbox.Add(self.list, 1, wx.EXPAND)
360 class ContextMenu(wx.Menu):
361 def __init__(self, item):
362 wx.Menu.__init__(self)
366 disconnect = wx.MenuItem(self, -1, 'Disconnect')
367 self.AppendItem(disconnect)
368 self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
370 def OnDisconnect(self, event):
371 vpn.disconnect(self.item[0])
373 def OnContext(self, event):
375 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
378 sortstate = self.list.GetSortState()
379 self.list.itemDataMap = {}
382 for key, connection in vpn.connections.items():
383 if self.list.GetItemCount() <= i:
384 self.list.InsertStringItem(i, connection.name)
386 self.list.SetStringItem(i, 0, connection.name)
387 self.list.SetStringItem(i, 1, connection.address)
388 self.list.SetStringItem(i, 2, connection.port)
389 self.list.SetStringItem(i, 3, str(connection.options))
390 self.list.SetStringItem(i, 4, str(connection.weight))
391 self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options, connection.weight)
392 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnContext)
393 self.list.SetItemData(i, i)
396 while self.list.GetItemCount() > i:
397 self.list.DeleteItem(self.list.GetItemCount() - 1)
399 self.list.SortListItems(sortstate[0], sortstate[1])
401 class NodesPage(wx.Panel):
402 def __init__(self, parent, id):
403 wx.Panel.__init__(self, parent, id)
404 self.list = SuperListCtrl(self, id)
405 self.list.InsertColumn( 0, 'Name')
406 self.list.InsertColumn( 1, 'Address')
407 self.list.InsertColumn( 2, 'Port')
408 self.list.InsertColumn( 3, 'Cipher')
409 self.list.InsertColumn( 4, 'Digest')
410 self.list.InsertColumn( 5, 'MACLength')
411 self.list.InsertColumn( 6, 'Compression')
412 self.list.InsertColumn( 7, 'Options')
413 self.list.InsertColumn( 8, 'Status')
414 self.list.InsertColumn( 9, 'Nexthop')
415 self.list.InsertColumn(10, 'Via')
416 self.list.InsertColumn(11, 'Distance')
417 self.list.InsertColumn(12, 'PMTU')
418 self.list.InsertColumn(13, 'Min MTU')
419 self.list.InsertColumn(14, 'Max MTU')
420 self.list.InsertColumn(15, 'Since')
422 hbox = wx.BoxSizer(wx.HORIZONTAL)
423 hbox.Add(self.list, 1, wx.EXPAND)
428 sortstate = self.list.GetSortState()
429 self.list.itemDataMap = {}
432 for key, node in vpn.nodes.items():
433 if self.list.GetItemCount() <= i:
434 self.list.InsertStringItem(i, node.name)
436 self.list.SetStringItem(i, 0, node.name)
437 self.list.SetStringItem(i, 1, node.address)
438 self.list.SetStringItem(i, 2, node.port)
439 self.list.SetStringItem(i, 3, str(node.cipher))
440 self.list.SetStringItem(i, 4, str(node.digest))
441 self.list.SetStringItem(i, 5, str(node.maclength))
442 self.list.SetStringItem(i, 6, str(node.compression))
443 self.list.SetStringItem(i, 7, format(node.options, "x"))
444 self.list.SetStringItem(i, 8, format(node.status, "04x"))
445 self.list.SetStringItem(i, 9, node.nexthop)
446 self.list.SetStringItem(i, 10, node.via)
447 self.list.SetStringItem(i, 11, str(node.distance))
448 self.list.SetStringItem(i, 12, str(node.pmtu))
449 self.list.SetStringItem(i, 13, str(node.minmtu))
450 self.list.SetStringItem(i, 14, str(node.maxmtu))
451 if node.last_state_change:
452 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
455 self.list.SetStringItem(i, 15, since)
456 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)
457 self.list.SetItemData(i, i)
460 while self.list.GetItemCount() > i:
461 self.list.DeleteItem(self.list.GetItemCount() - 1)
463 self.list.SortListItems(sortstate[0], sortstate[1])
465 class EdgesPage(wx.Panel):
466 def __init__(self, parent, id):
467 wx.Panel.__init__(self, parent, id)
468 self.list = SuperListCtrl(self, id)
469 self.list.InsertColumn(0, 'From')
470 self.list.InsertColumn(1, 'To')
471 self.list.InsertColumn(2, 'Address')
472 self.list.InsertColumn(3, 'Port')
473 self.list.InsertColumn(4, 'Options')
474 self.list.InsertColumn(5, 'Weight')
476 hbox = wx.BoxSizer(wx.HORIZONTAL)
477 hbox.Add(self.list, 1, wx.EXPAND)
482 sortstate = self.list.GetSortState()
483 self.list.itemDataMap = {}
486 for key, edge in vpn.edges.items():
487 if self.list.GetItemCount() <= i:
488 self.list.InsertStringItem(i, edge.fr)
490 self.list.SetStringItem(i, 0, edge.fr)
491 self.list.SetStringItem(i, 1, edge.to)
492 self.list.SetStringItem(i, 2, edge.address)
493 self.list.SetStringItem(i, 3, edge.port)
494 self.list.SetStringItem(i, 4, format(edge.options, "x"))
495 self.list.SetStringItem(i, 5, str(edge.weight))
496 self.list.itemDataMap[i] = (edge.fr, edge.to, edge.address, edge.port, edge.options, edge.weight)
497 self.list.SetItemData(i, i)
500 while self.list.GetItemCount() > i:
501 self.list.DeleteItem(self.list.GetItemCount() - 1)
503 self.list.SortListItems(sortstate[0], sortstate[1])
505 class SubnetsPage(wx.Panel):
506 def __init__(self, parent, id):
507 wx.Panel.__init__(self, parent, id)
508 self.list = SuperListCtrl(self, id)
509 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
510 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
511 self.list.InsertColumn(2, 'Owner')
512 hbox = wx.BoxSizer(wx.HORIZONTAL)
513 hbox.Add(self.list, 1, wx.EXPAND)
518 sortstate = self.list.GetSortState()
519 self.list.itemDataMap = {}
522 for key, subnet in vpn.subnets.items():
523 if self.list.GetItemCount() <= i:
524 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
526 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
527 self.list.SetStringItem(i, 1, subnet.weight)
528 self.list.SetStringItem(i, 2, subnet.owner)
529 self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
530 self.list.SetItemData(i, i)
533 while self.list.GetItemCount() > i:
534 self.list.DeleteItem(self.list.GetItemCount() - 1)
536 self.list.SortListItems(sortstate[0], sortstate[1])
538 class StatusPage(wx.Panel):
539 def __init__(self, parent, id):
540 wx.Panel.__init__(self, parent, id)
542 class GraphPage(wx.Window):
543 def __init__(self, parent, id):
544 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')
563 #self.AddPage(self.graph, 'Graph')
566 class MainWindow(wx.Frame):
567 def OnQuit(self, event):
570 def OnTimer(self, event):
572 self.np.nodes.refresh()
573 self.np.subnets.refresh()
574 self.np.edges.refresh()
575 self.np.connections.refresh()
577 def __init__(self, parent, id, title):
578 wx.Frame.__init__(self, parent, id, title)
580 menubar = wx.MenuBar()
582 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
583 menubar.Append(file, '&File')
585 #nb = wx.Notebook(self, -1)
586 #nb.SetPadding((0, 0))
587 self.np = NetPage(self, -1)
588 #nb.AddPage(np, 'VPN')
590 self.timer = wx.Timer(self, -1)
591 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
592 self.timer.Start(1000)
593 self.Bind(wx.EVT_MENU, self.OnQuit, id=1)
594 self.SetMenuBar(menubar)
598 mw = MainWindow(None, -1, 'Tinc GUI')
600 #def OnTaskBarIcon(event):
603 #icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
604 #taskbaricon = wx.TaskBarIcon()
605 #taskbaricon.SetIcon(icon, 'Tinc GUI')
606 #wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)