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
55 def parse(self, args):
57 self.address = args[1]
59 self.cipher = int(args[4])
60 self.digest = int(args[5])
61 self.maclength = int(args[6])
62 self.compression = int(args[7])
63 self.options = int(args[8], 0x10)
64 self.status = int(args[9], 0x10)
65 self.nexthop = args[10]
67 self.distance = int(args[12])
68 self.pmtu = int(args[13])
69 self.minmtu = int(args[14])
70 self.maxmtu = int(args[15])
71 self.last_state_change = float(args[16])
76 def parse(self, args):
79 self.address = args[2]
81 self.options = int(args[5], 16)
82 self.weight = int(args[6])
85 def parse(self, args):
86 if args[0].find('#') >= 0:
87 (address, self.weight) = args[0].split('#', 1)
92 if address.find('/') >= 0:
93 (self.address, self.prefixlen) = address.split('/', 1)
95 self.address = address
101 def parse(self, args):
103 self.address = args[1]
105 self.options = int(args[4], 0x10)
106 self.socket = int(args[5])
107 self.status = int(args[6], 0x10)
111 confdir = '/etc/tinc'
116 f = open(self.pidfile)
117 info = string.split(f.readline())
120 # check if there is a UNIX socket as well
121 if self.pidfile.endswith(".pid"):
122 unixfile = self.pidfile.replace(".pid", ".socket");
124 unixfile = self.pidfile + ".socket";
126 if os.path.exists(unixfile):
127 # use it if it exists
128 print(unixfile + " exists!");
129 s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
132 # otherwise connect via TCP
133 print(unixfile + " does not exist.");
138 s = socket.socket(af, socket.SOCK_STREAM)
139 s.connect((info[2], int(info[4])))
141 self.sf = s.makefile()
143 hello = string.split(self.sf.readline())
145 self.sf.write('0 ^' + info[1] + ' 17\r\n')
147 resp = string.split(self.sf.readline())
152 self.connections = {}
156 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
159 for node in self.nodes.values():
161 for edge in self.edges.values():
163 for subnet in self.subnets.values():
164 subnet.visited = False
165 for connections in self.connections.values():
166 connections.visited = False
169 resp = string.split(self.sf.readline())
177 node = self.nodes.get(resp[2]) or Node()
180 self.nodes[resp[2]] = node
184 edge = self.nodes.get((resp[2], resp[3])) or Edge()
187 self.edges[(resp[2], resp[3])] = edge
191 subnet = self.subnets.get((resp[2], resp[3])) or Subnet()
192 subnet.parse(resp[2:])
193 subnet.visited = True
194 self.subnets[(resp[2], resp[3])] = subnet
195 self.nodes[subnet.owner].subnets[resp[2]] = subnet
199 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection()
200 connection.parse(resp[2:])
201 connection.visited = True
202 self.connections[(resp[2], resp[3], resp[5])] = connection
206 for key, subnet in self.subnets.items():
207 if not subnet.visited:
208 del self.subnets[key]
210 for key, edge in self.edges.items():
214 for key, node in self.nodes.items():
218 for key, subnet in node.subnets.items():
219 if not subnet.visited:
220 del node.subnets[key]
222 for key, connection in self.connections.items():
223 if not connection.visited:
224 del self.connections[key]
229 def disconnect(self, name):
230 self.sf.write('18 12 ' + name + '\r\n')
232 resp = string.split(self.sf.readline())
234 def debug(self, level = -1):
235 self.sf.write('18 9 ' + str(level) + '\r\n')
237 resp = string.split(self.sf.readline())
240 def __init__(self, netname = None, pidfile = None):
241 if platform.system() == 'Windows':
242 sam = _winreg.KEY_READ
243 if platform.machine().endswith('64'):
244 sam = sam | _winreg.KEY_WOW64_64KEY
246 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
248 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
250 key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
251 VPN.confdir = _winreg.QueryValue(key, None)
256 self.netname = netname
257 self.confbase = os.path.join(VPN.confdir, netname)
259 self.confbase = VPN.confdir
261 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
264 self.pidfile = pidfile
266 if platform.system() == 'Windows':
267 self.pidfile = os.path.join(self.confbase, 'pid')
270 self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
272 self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
281 def usage(exitcode = 0):
282 print('Usage: ' + argv0 + ' [options]')
283 print('\nValid options are:')
284 print(' -n, --net=NETNAME Connect to net NETNAME.')
285 print(' --pidfile=FILENAME Read control cookie from FILENAME.')
286 print(' --help Display this help and exit.')
287 print('\nReport bugs to tinc@tinc-vpn.org.')
291 if sys.argv[0] in ('-n', '--net'):
293 netname = sys.argv[0]
294 elif sys.argv[0] in ('--pidfile'):
296 pidfile = sys.argv[0]
297 elif sys.argv[0] in ('--help'):
300 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
306 netname = os.getenv("NETNAME")
311 vpn = VPN(netname, pidfile)
314 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
315 def __init__(self, parent, style):
316 wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
317 ListCtrlAutoWidthMixin.__init__(self)
318 ColumnSorterMixin.__init__(self, 16)
320 def GetListCtrl(self):
324 class SettingsPage(wx.Panel):
325 def OnDebugLevel(self, event):
326 vpn.debug(self.debug.GetValue())
328 def __init__(self, parent, id):
329 wx.Panel.__init__(self, parent, id)
330 grid = wx.FlexGridSizer(cols = 2)
331 grid.AddGrowableCol(1, 1)
333 namelabel = wx.StaticText(self, -1, 'Name:')
334 self.name = wx.TextCtrl(self, -1, vpn.name)
336 grid.Add(self.name, 1, wx.EXPAND)
338 portlabel = wx.StaticText(self, -1, 'Port:')
339 self.port = wx.TextCtrl(self, -1, vpn.port)
343 debuglabel = wx.StaticText(self, -1, 'Debug level:')
344 self.debug = wx.SpinCtrl(self, min = 0, max = 5, initial = vpn.debug())
345 self.debug.Bind(wx.EVT_SPINCTRL, self.OnDebugLevel)
349 modelabel = wx.StaticText(self, -1, 'Mode:')
350 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
356 class ConnectionsPage(wx.Panel):
357 def __init__(self, parent, id):
358 wx.Panel.__init__(self, parent, id)
359 self.list = SuperListCtrl(self, id)
360 self.list.InsertColumn(0, 'Name')
361 self.list.InsertColumn(1, 'Address')
362 self.list.InsertColumn(2, 'Port')
363 self.list.InsertColumn(3, 'Options')
364 self.list.InsertColumn(4, 'Weight')
366 hbox = wx.BoxSizer(wx.HORIZONTAL)
367 hbox.Add(self.list, 1, wx.EXPAND)
371 class ContextMenu(wx.Menu):
372 def __init__(self, item):
373 wx.Menu.__init__(self)
377 disconnect = wx.MenuItem(self, -1, 'Disconnect')
378 self.AppendItem(disconnect)
379 self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
381 def OnDisconnect(self, event):
382 vpn.disconnect(self.item[0])
384 def OnContext(self, event):
386 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
389 sortstate = self.list.GetSortState()
390 self.list.itemDataMap = {}
393 for key, connection in vpn.connections.items():
394 if self.list.GetItemCount() <= i:
395 self.list.InsertStringItem(i, connection.name)
397 self.list.SetStringItem(i, 0, connection.name)
398 self.list.SetStringItem(i, 1, connection.address)
399 self.list.SetStringItem(i, 2, connection.port)
400 self.list.SetStringItem(i, 3, str(connection.options))
401 self.list.SetStringItem(i, 4, str(connection.weight))
402 self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options, connection.weight)
403 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnContext)
404 self.list.SetItemData(i, i)
407 while self.list.GetItemCount() > i:
408 self.list.DeleteItem(self.list.GetItemCount() - 1)
410 self.list.SortListItems(sortstate[0], sortstate[1])
412 class NodesPage(wx.Panel):
413 def __init__(self, parent, id):
414 wx.Panel.__init__(self, parent, id)
415 self.list = SuperListCtrl(self, id)
416 self.list.InsertColumn( 0, 'Name')
417 self.list.InsertColumn( 1, 'Address')
418 self.list.InsertColumn( 2, 'Port')
419 self.list.InsertColumn( 3, 'Cipher')
420 self.list.InsertColumn( 4, 'Digest')
421 self.list.InsertColumn( 5, 'MACLength')
422 self.list.InsertColumn( 6, 'Compression')
423 self.list.InsertColumn( 7, 'Options')
424 self.list.InsertColumn( 8, 'Status')
425 self.list.InsertColumn( 9, 'Nexthop')
426 self.list.InsertColumn(10, 'Via')
427 self.list.InsertColumn(11, 'Distance')
428 self.list.InsertColumn(12, 'PMTU')
429 self.list.InsertColumn(13, 'Min MTU')
430 self.list.InsertColumn(14, 'Max MTU')
431 self.list.InsertColumn(15, 'Since')
433 hbox = wx.BoxSizer(wx.HORIZONTAL)
434 hbox.Add(self.list, 1, wx.EXPAND)
439 sortstate = self.list.GetSortState()
440 self.list.itemDataMap = {}
443 for key, node in vpn.nodes.items():
444 if self.list.GetItemCount() <= i:
445 self.list.InsertStringItem(i, node.name)
447 self.list.SetStringItem(i, 0, node.name)
448 self.list.SetStringItem(i, 1, node.address)
449 self.list.SetStringItem(i, 2, node.port)
450 self.list.SetStringItem(i, 3, str(node.cipher))
451 self.list.SetStringItem(i, 4, str(node.digest))
452 self.list.SetStringItem(i, 5, str(node.maclength))
453 self.list.SetStringItem(i, 6, str(node.compression))
454 self.list.SetStringItem(i, 7, format(node.options, "x"))
455 self.list.SetStringItem(i, 8, format(node.status, "04x"))
456 self.list.SetStringItem(i, 9, node.nexthop)
457 self.list.SetStringItem(i, 10, node.via)
458 self.list.SetStringItem(i, 11, str(node.distance))
459 self.list.SetStringItem(i, 12, str(node.pmtu))
460 self.list.SetStringItem(i, 13, str(node.minmtu))
461 self.list.SetStringItem(i, 14, str(node.maxmtu))
462 if node.last_state_change:
463 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
466 self.list.SetStringItem(i, 15, since)
467 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)
468 self.list.SetItemData(i, i)
471 while self.list.GetItemCount() > i:
472 self.list.DeleteItem(self.list.GetItemCount() - 1)
474 self.list.SortListItems(sortstate[0], sortstate[1])
476 class EdgesPage(wx.Panel):
477 def __init__(self, parent, id):
478 wx.Panel.__init__(self, parent, id)
479 self.list = SuperListCtrl(self, id)
480 self.list.InsertColumn(0, 'From')
481 self.list.InsertColumn(1, 'To')
482 self.list.InsertColumn(2, 'Address')
483 self.list.InsertColumn(3, 'Port')
484 self.list.InsertColumn(4, 'Options')
485 self.list.InsertColumn(5, 'Weight')
487 hbox = wx.BoxSizer(wx.HORIZONTAL)
488 hbox.Add(self.list, 1, wx.EXPAND)
493 sortstate = self.list.GetSortState()
494 self.list.itemDataMap = {}
497 for key, edge in vpn.edges.items():
498 if self.list.GetItemCount() <= i:
499 self.list.InsertStringItem(i, edge.fr)
501 self.list.SetStringItem(i, 0, edge.fr)
502 self.list.SetStringItem(i, 1, edge.to)
503 self.list.SetStringItem(i, 2, edge.address)
504 self.list.SetStringItem(i, 3, edge.port)
505 self.list.SetStringItem(i, 4, format(edge.options, "x"))
506 self.list.SetStringItem(i, 5, str(edge.weight))
507 self.list.itemDataMap[i] = (edge.fr, edge.to, edge.address, edge.port, edge.options, edge.weight)
508 self.list.SetItemData(i, i)
511 while self.list.GetItemCount() > i:
512 self.list.DeleteItem(self.list.GetItemCount() - 1)
514 self.list.SortListItems(sortstate[0], sortstate[1])
516 class SubnetsPage(wx.Panel):
517 def __init__(self, parent, id):
518 wx.Panel.__init__(self, parent, id)
519 self.list = SuperListCtrl(self, id)
520 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
521 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
522 self.list.InsertColumn(2, 'Owner')
523 hbox = wx.BoxSizer(wx.HORIZONTAL)
524 hbox.Add(self.list, 1, wx.EXPAND)
529 sortstate = self.list.GetSortState()
530 self.list.itemDataMap = {}
533 for key, subnet in vpn.subnets.items():
534 if self.list.GetItemCount() <= i:
535 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
537 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
538 self.list.SetStringItem(i, 1, subnet.weight)
539 self.list.SetStringItem(i, 2, subnet.owner)
540 self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
541 self.list.SetItemData(i, i)
544 while self.list.GetItemCount() > i:
545 self.list.DeleteItem(self.list.GetItemCount() - 1)
547 self.list.SortListItems(sortstate[0], sortstate[1])
549 class StatusPage(wx.Panel):
550 def __init__(self, parent, id):
551 wx.Panel.__init__(self, parent, id)
553 class GraphPage(wx.Window):
554 def __init__(self, parent, id):
555 wx.Window.__init__(self, parent, id)
557 class NetPage(wx.Notebook):
558 def __init__(self, parent, id):
559 wx.Notebook.__init__(self, parent)
560 self.settings = SettingsPage(self, id)
561 self.connections = ConnectionsPage(self, id)
562 self.nodes = NodesPage(self, id)
563 self.edges = EdgesPage(self, id)
564 self.subnets = SubnetsPage(self, id)
565 self.graph = GraphPage(self, id)
566 self.status = StatusPage(self, id)
568 self.AddPage(self.settings, 'Settings')
569 #self.AddPage(self.status, 'Status')
570 self.AddPage(self.connections, 'Connections')
571 self.AddPage(self.nodes, 'Nodes')
572 self.AddPage(self.edges, 'Edges')
573 self.AddPage(self.subnets, 'Subnets')
574 #self.AddPage(self.graph, 'Graph')
577 class MainWindow(wx.Frame):
578 def OnQuit(self, event):
581 def OnTimer(self, event):
583 self.np.nodes.refresh()
584 self.np.subnets.refresh()
585 self.np.edges.refresh()
586 self.np.connections.refresh()
588 def __init__(self, parent, id, title):
589 wx.Frame.__init__(self, parent, id, title)
591 menubar = wx.MenuBar()
593 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
594 menubar.Append(file, '&File')
596 #nb = wx.Notebook(self, -1)
597 #nb.SetPadding((0, 0))
598 self.np = NetPage(self, -1)
599 #nb.AddPage(np, 'VPN')
601 self.timer = wx.Timer(self, -1)
602 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
603 self.timer.Start(1000)
604 self.Bind(wx.EVT_MENU, self.OnQuit, id=1)
605 self.SetMenuBar(menubar)
609 mw = MainWindow(None, -1, 'Tinc GUI')
611 #def OnTaskBarIcon(event):
614 #icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
615 #taskbaricon = wx.TaskBarIcon()
616 #taskbaricon.SetIcon(icon, 'Tinc GUI')
617 #wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)