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[-2], 16)
82 self.weight = int(args[-1])
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 if subnet.owner == "(broadcast)":
197 self.nodes[subnet.owner].subnets[resp[2]] = subnet
201 connection = self.connections.get((resp[2], resp[3], resp[5])) or Connection()
202 connection.parse(resp[2:])
203 connection.visited = True
204 self.connections[(resp[2], resp[3], resp[5])] = connection
208 for key, subnet in self.subnets.items():
209 if not subnet.visited:
210 del self.subnets[key]
212 for key, edge in self.edges.items():
216 for key, node in self.nodes.items():
220 for key, subnet in node.subnets.items():
221 if not subnet.visited:
222 del node.subnets[key]
224 for key, connection in self.connections.items():
225 if not connection.visited:
226 del self.connections[key]
231 def disconnect(self, name):
232 self.sf.write('18 12 ' + name + '\r\n')
234 resp = string.split(self.sf.readline())
236 def debug(self, level = -1):
237 self.sf.write('18 9 ' + str(level) + '\r\n')
239 resp = string.split(self.sf.readline())
242 def __init__(self, netname = None, pidfile = None):
243 if platform.system() == 'Windows':
244 sam = _winreg.KEY_READ
245 if platform.machine().endswith('64'):
246 sam = sam | _winreg.KEY_WOW64_64KEY
248 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
250 key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
252 key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
253 VPN.confdir = _winreg.QueryValue(key, None)
258 self.netname = netname
259 self.confbase = os.path.join(VPN.confdir, netname)
261 self.confbase = VPN.confdir
263 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
266 self.pidfile = pidfile
268 if platform.system() == 'Windows':
269 self.pidfile = os.path.join(self.confbase, 'pid')
272 self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
274 self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
283 def usage(exitcode = 0):
284 print('Usage: ' + argv0 + ' [options]')
285 print('\nValid options are:')
286 print(' -n, --net=NETNAME Connect to net NETNAME.')
287 print(' --pidfile=FILENAME Read control cookie from FILENAME.')
288 print(' --help Display this help and exit.')
289 print('\nReport bugs to tinc@tinc-vpn.org.')
293 if sys.argv[0] in ('-n', '--net'):
295 netname = sys.argv[0]
296 elif sys.argv[0] in ('--pidfile'):
298 pidfile = sys.argv[0]
299 elif sys.argv[0] in ('--help'):
302 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
308 netname = os.getenv("NETNAME")
313 vpn = VPN(netname, pidfile)
316 class SuperListCtrl(wx.ListCtrl, ColumnSorterMixin, ListCtrlAutoWidthMixin):
317 def __init__(self, parent, style):
318 wx.ListCtrl.__init__(self, parent, -1, style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
319 ListCtrlAutoWidthMixin.__init__(self)
320 ColumnSorterMixin.__init__(self, 16)
322 def GetListCtrl(self):
326 class SettingsPage(wx.Panel):
327 def OnDebugLevel(self, event):
328 vpn.debug(self.debug.GetValue())
330 def __init__(self, parent, id):
331 wx.Panel.__init__(self, parent, id)
332 grid = wx.FlexGridSizer(cols = 2)
333 grid.AddGrowableCol(1, 1)
335 namelabel = wx.StaticText(self, -1, 'Name:')
336 self.name = wx.TextCtrl(self, -1, vpn.name)
338 grid.Add(self.name, 1, wx.EXPAND)
340 portlabel = wx.StaticText(self, -1, 'Port:')
341 self.port = wx.TextCtrl(self, -1, vpn.port)
345 debuglabel = wx.StaticText(self, -1, 'Debug level:')
346 self.debug = wx.SpinCtrl(self, min = 0, max = 5, initial = vpn.debug())
347 self.debug.Bind(wx.EVT_SPINCTRL, self.OnDebugLevel)
351 modelabel = wx.StaticText(self, -1, 'Mode:')
352 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
358 class ConnectionsPage(wx.Panel):
359 def __init__(self, parent, id):
360 wx.Panel.__init__(self, parent, id)
361 self.list = SuperListCtrl(self, id)
362 self.list.InsertColumn(0, 'Name')
363 self.list.InsertColumn(1, 'Address')
364 self.list.InsertColumn(2, 'Port')
365 self.list.InsertColumn(3, 'Options')
366 self.list.InsertColumn(4, 'Weight')
368 hbox = wx.BoxSizer(wx.HORIZONTAL)
369 hbox.Add(self.list, 1, wx.EXPAND)
373 class ContextMenu(wx.Menu):
374 def __init__(self, item):
375 wx.Menu.__init__(self)
379 disconnect = wx.MenuItem(self, -1, 'Disconnect')
380 self.AppendItem(disconnect)
381 self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
383 def OnDisconnect(self, event):
384 vpn.disconnect(self.item[0])
386 def OnContext(self, event):
388 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
391 sortstate = self.list.GetSortState()
392 self.list.itemDataMap = {}
395 for key, connection in vpn.connections.items():
396 if self.list.GetItemCount() <= i:
397 self.list.InsertStringItem(i, connection.name)
399 self.list.SetStringItem(i, 0, connection.name)
400 self.list.SetStringItem(i, 1, connection.address)
401 self.list.SetStringItem(i, 2, connection.port)
402 self.list.SetStringItem(i, 3, str(connection.options))
403 self.list.SetStringItem(i, 4, str(connection.weight))
404 self.list.itemDataMap[i] = (connection.name, connection.address, connection.port, connection.options, connection.weight)
405 self.list.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self.OnContext)
406 self.list.SetItemData(i, i)
409 while self.list.GetItemCount() > i:
410 self.list.DeleteItem(self.list.GetItemCount() - 1)
412 self.list.SortListItems(sortstate[0], sortstate[1])
414 class NodesPage(wx.Panel):
415 def __init__(self, parent, id):
416 wx.Panel.__init__(self, parent, id)
417 self.list = SuperListCtrl(self, id)
418 self.list.InsertColumn( 0, 'Name')
419 self.list.InsertColumn( 1, 'Address')
420 self.list.InsertColumn( 2, 'Port')
421 self.list.InsertColumn( 3, 'Cipher')
422 self.list.InsertColumn( 4, 'Digest')
423 self.list.InsertColumn( 5, 'MACLength')
424 self.list.InsertColumn( 6, 'Compression')
425 self.list.InsertColumn( 7, 'Options')
426 self.list.InsertColumn( 8, 'Status')
427 self.list.InsertColumn( 9, 'Nexthop')
428 self.list.InsertColumn(10, 'Via')
429 self.list.InsertColumn(11, 'Distance')
430 self.list.InsertColumn(12, 'PMTU')
431 self.list.InsertColumn(13, 'Min MTU')
432 self.list.InsertColumn(14, 'Max MTU')
433 self.list.InsertColumn(15, 'Since')
435 hbox = wx.BoxSizer(wx.HORIZONTAL)
436 hbox.Add(self.list, 1, wx.EXPAND)
441 sortstate = self.list.GetSortState()
442 self.list.itemDataMap = {}
445 for key, node in vpn.nodes.items():
446 if self.list.GetItemCount() <= i:
447 self.list.InsertStringItem(i, node.name)
449 self.list.SetStringItem(i, 0, node.name)
450 self.list.SetStringItem(i, 1, node.address)
451 self.list.SetStringItem(i, 2, node.port)
452 self.list.SetStringItem(i, 3, str(node.cipher))
453 self.list.SetStringItem(i, 4, str(node.digest))
454 self.list.SetStringItem(i, 5, str(node.maclength))
455 self.list.SetStringItem(i, 6, str(node.compression))
456 self.list.SetStringItem(i, 7, format(node.options, "x"))
457 self.list.SetStringItem(i, 8, format(node.status, "04x"))
458 self.list.SetStringItem(i, 9, node.nexthop)
459 self.list.SetStringItem(i, 10, node.via)
460 self.list.SetStringItem(i, 11, str(node.distance))
461 self.list.SetStringItem(i, 12, str(node.pmtu))
462 self.list.SetStringItem(i, 13, str(node.minmtu))
463 self.list.SetStringItem(i, 14, str(node.maxmtu))
464 if node.last_state_change:
465 since = time.strftime("%Y-%m-%d %H:%M", time.localtime(node.last_state_change))
468 self.list.SetStringItem(i, 15, since)
469 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)
470 self.list.SetItemData(i, i)
473 while self.list.GetItemCount() > i:
474 self.list.DeleteItem(self.list.GetItemCount() - 1)
476 self.list.SortListItems(sortstate[0], sortstate[1])
478 class EdgesPage(wx.Panel):
479 def __init__(self, parent, id):
480 wx.Panel.__init__(self, parent, id)
481 self.list = SuperListCtrl(self, id)
482 self.list.InsertColumn(0, 'From')
483 self.list.InsertColumn(1, 'To')
484 self.list.InsertColumn(2, 'Address')
485 self.list.InsertColumn(3, 'Port')
486 self.list.InsertColumn(4, 'Options')
487 self.list.InsertColumn(5, 'Weight')
489 hbox = wx.BoxSizer(wx.HORIZONTAL)
490 hbox.Add(self.list, 1, wx.EXPAND)
495 sortstate = self.list.GetSortState()
496 self.list.itemDataMap = {}
499 for key, edge in vpn.edges.items():
500 if self.list.GetItemCount() <= i:
501 self.list.InsertStringItem(i, edge.fr)
503 self.list.SetStringItem(i, 0, edge.fr)
504 self.list.SetStringItem(i, 1, edge.to)
505 self.list.SetStringItem(i, 2, edge.address)
506 self.list.SetStringItem(i, 3, edge.port)
507 self.list.SetStringItem(i, 4, format(edge.options, "x"))
508 self.list.SetStringItem(i, 5, str(edge.weight))
509 self.list.itemDataMap[i] = (edge.fr, edge.to, edge.address, edge.port, edge.options, edge.weight)
510 self.list.SetItemData(i, i)
513 while self.list.GetItemCount() > i:
514 self.list.DeleteItem(self.list.GetItemCount() - 1)
516 self.list.SortListItems(sortstate[0], sortstate[1])
518 class SubnetsPage(wx.Panel):
519 def __init__(self, parent, id):
520 wx.Panel.__init__(self, parent, id)
521 self.list = SuperListCtrl(self, id)
522 self.list.InsertColumn(0, 'Subnet', wx.LIST_FORMAT_RIGHT)
523 self.list.InsertColumn(1, 'Weight', wx.LIST_FORMAT_RIGHT)
524 self.list.InsertColumn(2, 'Owner')
525 hbox = wx.BoxSizer(wx.HORIZONTAL)
526 hbox.Add(self.list, 1, wx.EXPAND)
531 sortstate = self.list.GetSortState()
532 self.list.itemDataMap = {}
535 for key, subnet in vpn.subnets.items():
536 if self.list.GetItemCount() <= i:
537 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
539 self.list.SetStringItem(i, 0, subnet.address + '/' + subnet.prefixlen)
540 self.list.SetStringItem(i, 1, str(subnet.weight))
541 self.list.SetStringItem(i, 2, subnet.owner)
542 self.list.itemDataMap[i] = (subnet.address + '/' + subnet.prefixlen, subnet.weight, subnet.owner)
543 self.list.SetItemData(i, i)
546 while self.list.GetItemCount() > i:
547 self.list.DeleteItem(self.list.GetItemCount() - 1)
549 self.list.SortListItems(sortstate[0], sortstate[1])
551 class StatusPage(wx.Panel):
552 def __init__(self, parent, id):
553 wx.Panel.__init__(self, parent, id)
555 class GraphPage(wx.Window):
556 def __init__(self, parent, id):
557 wx.Window.__init__(self, parent, id)
559 class NetPage(wx.Notebook):
560 def __init__(self, parent, id):
561 wx.Notebook.__init__(self, parent)
562 self.settings = SettingsPage(self, id)
563 self.connections = ConnectionsPage(self, id)
564 self.nodes = NodesPage(self, id)
565 self.edges = EdgesPage(self, id)
566 self.subnets = SubnetsPage(self, id)
567 self.graph = GraphPage(self, id)
568 self.status = StatusPage(self, id)
570 self.AddPage(self.settings, 'Settings')
571 #self.AddPage(self.status, 'Status')
572 self.AddPage(self.connections, 'Connections')
573 self.AddPage(self.nodes, 'Nodes')
574 self.AddPage(self.edges, 'Edges')
575 self.AddPage(self.subnets, 'Subnets')
576 #self.AddPage(self.graph, 'Graph')
579 class MainWindow(wx.Frame):
580 def OnQuit(self, event):
583 def OnTimer(self, event):
585 self.np.nodes.refresh()
586 self.np.subnets.refresh()
587 self.np.edges.refresh()
588 self.np.connections.refresh()
590 def __init__(self, parent, id, title):
591 wx.Frame.__init__(self, parent, id, title)
593 menubar = wx.MenuBar()
595 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
596 menubar.Append(file, '&File')
598 #nb = wx.Notebook(self, -1)
599 #nb.SetPadding((0, 0))
600 self.np = NetPage(self, -1)
601 #nb.AddPage(np, 'VPN')
603 self.timer = wx.Timer(self, -1)
604 self.Bind(wx.EVT_TIMER, self.OnTimer, self.timer)
605 self.timer.Start(1000)
606 self.Bind(wx.EVT_MENU, self.OnQuit, id=1)
607 self.SetMenuBar(menubar)
611 mw = MainWindow(None, -1, 'Tinc GUI')
613 #def OnTaskBarIcon(event):
616 #icon = wx.Icon("tincgui.ico", wx.BITMAP_TYPE_PNG)
617 #taskbaricon = wx.TaskBarIcon()
618 #taskbaricon.SetIcon(icon, 'Tinc GUI')
619 #wx.EVT_TASKBAR_RIGHT_UP(taskbaricon, OnTaskBarIcon)