Merge remote-tracking branch 'groxxda/gui-fixes' into 1.1
[tinc] / gui / tinc-gui
1 #!/usr/bin/env python
2
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>
6 #
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.
11 #
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.
16 #
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.
20
21 import string
22 import socket
23 import wx
24 import sys
25 import os
26 import platform
27 import time
28 from wx.lib.mixins.listctrl import ColumnSorterMixin
29 from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
30
31 if platform.system() == 'Windows':
32         import _winreg
33
34 # Classes to interface with a running tinc daemon
35
36 REQ_STOP = 0
37 REQ_RELOAD = 1
38 REQ_RESTART = 2
39 REQ_DUMP_NODES = 3
40 REQ_DUMP_EDGES = 4
41 REQ_DUMP_SUBNETS = 5
42 REQ_DUMP_CONNECTIONS = 6
43 REQ_DUMP_GRAPH = 7
44 REQ_PURGE = 8
45 REQ_SET_DEBUG = 9
46 REQ_RETRY = 10
47 REQ_CONNECT = 11
48 REQ_DISCONNECT = 12
49
50 ID = 0
51 ACK = 4
52 CONTROL = 18
53
54 class Node:
55         def parse(self, args):
56                 self.name = args[0]
57                 self.address = args[1]
58                 self.port = args[3]
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]
66                 self.via = args[11]
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])
72
73                 self.subnets = {}
74
75 class Edge:
76         def parse(self, args):
77                 self.fr = args[0]
78                 self.to = args[1]
79                 self.address = args[2]
80                 self.port = args[4]
81                 self.options = int(args[-2], 16)
82                 self.weight = int(args[-1])
83
84 class Subnet:
85         def parse(self, args):
86                 if args[0].find('#') >= 0:
87                         (address, self.weight) = args[0].split('#', 1)
88                 else:
89                         self.weight = 10
90                         address = args[0]
91
92                 if address.find('/') >= 0:
93                         (self.address, self.prefixlen) = address.split('/', 1)
94                 else:
95                         self.address = address
96                         self.prefixlen = '48'
97
98                 self.owner = args[1]    
99
100 class Connection:
101         def parse(self, args):
102                 self.name = args[0]
103                 self.address = args[1]
104                 self.port = args[3]
105                 self.options = int(args[4], 0x10)
106                 self.socket = int(args[5])
107                 self.status = int(args[6], 0x10)
108                 self.weight = 123
109
110 class VPN:
111         confdir = '/etc/tinc'
112         piddir = '/var/run/'
113
114         def connect(self):
115                 # read the pidfile
116                 f = open(self.pidfile)
117                 info = string.split(f.readline())
118                 f.close()
119
120                 # check if there is a UNIX socket as well
121                 if self.pidfile.endswith(".pid"):
122                         unixfile = self.pidfile.replace(".pid", ".socket");
123                 else:
124                         unixfile = self.pidfile + ".socket";
125
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)
130                         s.connect(unixfile)
131                 else:
132                         # otherwise connect via TCP
133                         print(unixfile + " does not exist.");
134                         if ':' in info[2]:
135                                 af = socket.AF_INET6
136                         else:
137                                 af = socket.AF_INET
138                         s = socket.socket(af, socket.SOCK_STREAM)
139                         s.connect((info[2], int(info[4])))
140
141                 self.sf = s.makefile()
142                 s.close()
143                 hello = string.split(self.sf.readline())
144                 self.name = hello[1]
145                 self.sf.write('0 ^' + info[1] + ' 17\r\n')
146                 self.sf.flush()
147                 resp = string.split(self.sf.readline())
148                 self.port = info[4]
149                 self.nodes = {}
150                 self.edges = {}
151                 self.subnets = {}
152                 self.connections = {}
153                 self.refresh()
154
155         def refresh(self):
156                 self.sf.write('18 3\r\n18 4\r\n18 5\r\n18 6\r\n')
157                 self.sf.flush()
158
159                 for node in self.nodes.values():
160                         node.visited = False
161                 for edge in self.edges.values():
162                         edge.visited = False
163                 for subnet in self.subnets.values():
164                         subnet.visited = False
165                 for connections in self.connections.values():
166                         connections.visited = False
167
168                 while True:
169                         resp = string.split(self.sf.readline())
170                         if len(resp) < 2:
171                                 break
172                         if resp[0] != '18':
173                                 break
174                         if resp[1] == '3':
175                                 if len(resp) < 19:
176                                         continue
177                                 node = self.nodes.get(resp[2]) or Node()
178                                 node.parse(resp[2:])
179                                 node.visited = True
180                                 self.nodes[resp[2]] = node
181                         elif resp[1] == '4':
182                                 if len(resp) < 9:
183                                         continue
184                                 edge = self.nodes.get((resp[2], resp[3])) or Edge()
185                                 edge.parse(resp[2:])
186                                 edge.visited = True
187                                 self.edges[(resp[2], resp[3])] = edge
188                         elif resp[1] == '5':
189                                 if len(resp) < 4:
190                                         continue
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)":
196                                         continue
197                                 self.nodes[subnet.owner].subnets[resp[2]] = subnet
198                         elif resp[1] == '6':
199                                 if len(resp) < 9:
200                                         break
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
205                         else:
206                                 break
207
208                 for key, subnet in self.subnets.items():
209                         if not subnet.visited:
210                                 del self.subnets[key]
211
212                 for key, edge in self.edges.items():
213                         if not edge.visited:
214                                 del self.edges[key]
215
216                 for key, node in self.nodes.items():
217                         if not node.visited:
218                                 del self.nodes[key]
219                         else:
220                                 for key, subnet in node.subnets.items():
221                                         if not subnet.visited:
222                                                 del node.subnets[key]
223
224                 for key, connection in self.connections.items():
225                         if not connection.visited:
226                                 del self.connections[key]
227
228         def close(self):
229                 self.sf.close()
230
231         def disconnect(self, name):
232                 self.sf.write('18 12 ' + name + '\r\n')
233                 self.sf.flush()
234                 resp = string.split(self.sf.readline())
235
236         def debug(self, level = -1):
237                 self.sf.write('18 9 ' + str(level) + '\r\n')
238                 self.sf.flush()
239                 resp = string.split(self.sf.readline())
240                 return int(resp[2])
241
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
247                         try:
248                                 reg = _winreg.ConnectRegistry(None, _winreg.HKEY_LOCAL_MACHINE)
249                                 try:
250                                         key = _winreg.OpenKey(reg, "SOFTWARE\\tinc", 0, sam)
251                                 except WindowsError:
252                                         key = _winreg.OpenKey(reg, "SOFTWARE\\Wow6432Node\\tinc", 0, sam)
253                                 VPN.confdir = _winreg.QueryValue(key, None)
254                         except WindowsError:
255                                 pass
256
257                 if netname:
258                         self.netname = netname
259                         self.confbase = os.path.join(VPN.confdir, netname)
260                 else:
261                         self.confbase = VPN.confdir
262
263                 self.tincconf = os.path.join(self.confbase, 'tinc.conf')
264
265                 if pidfile != None:
266                         self.pidfile = pidfile
267                 else:
268                         if platform.system() == 'Windows':
269                                 self.pidfile = os.path.join(self.confbase, 'pid')
270                         else:
271                                 if netname:
272                                         self.pidfile = os.path.join(VPN.piddir, 'tinc.' + netname + '.pid')
273                                 else:
274                                         self.pidfile = os.path.join(VPN.piddir, 'tinc.pid')
275
276 # GUI starts here
277
278 argv0 = sys.argv[0]
279 del sys.argv[0]
280 netname = None
281 pidfile = None
282
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.')
290         sys.exit(exitcode)
291
292 while sys.argv:
293         if sys.argv[0] in ('-n', '--net'):
294                 del sys.argv[0]
295                 netname = sys.argv[0]
296         elif sys.argv[0] in ('--pidfile'):
297                 del sys.argv[0]
298                 pidfile = sys.argv[0]
299         elif sys.argv[0] in ('--help'):
300                 usage(0)
301         else:
302                 print(argv0 + ': unrecognized option \'' + sys.argv[0] + '\'')
303                 usage(1)
304
305         del sys.argv[0]
306
307 if netname == None:
308         netname = os.getenv("NETNAME")
309
310 if netname == ".":
311         netname = None
312
313 vpn = VPN(netname, pidfile)
314 vpn.connect()
315
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)
321
322     def GetListCtrl(self):
323         return self
324
325
326 class SettingsPage(wx.Panel):
327         def OnDebugLevel(self, event):
328                 vpn.debug(self.debug.GetValue())
329
330         def __init__(self, parent, id):
331                 wx.Panel.__init__(self, parent, id)
332                 grid = wx.FlexGridSizer(cols = 2)
333                 grid.AddGrowableCol(1, 1)
334
335                 namelabel = wx.StaticText(self, -1, 'Name:')
336                 self.name = wx.TextCtrl(self, -1, vpn.name)
337                 grid.Add(namelabel)
338                 grid.Add(self.name, 1, wx.EXPAND)
339
340                 portlabel = wx.StaticText(self, -1, 'Port:')
341                 self.port = wx.TextCtrl(self, -1, vpn.port)
342                 grid.Add(portlabel)
343                 grid.Add(self.port)
344
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)
348                 grid.Add(debuglabel)
349                 grid.Add(self.debug)
350
351                 modelabel = wx.StaticText(self, -1, 'Mode:')
352                 self.mode = wx.ComboBox(self, -1, style = wx.CB_READONLY, value = 'Router', choices = ['Router', 'Switch', 'Hub'])
353                 grid.Add(modelabel)
354                 grid.Add(self.mode)
355
356                 self.SetSizer(grid)
357
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')
367
368                 hbox = wx.BoxSizer(wx.HORIZONTAL)
369                 hbox.Add(self.list, 1, wx.EXPAND)
370                 self.SetSizer(hbox)
371                 self.refresh()
372
373         class ContextMenu(wx.Menu):
374                 def __init__(self, item):
375                         wx.Menu.__init__(self)
376
377                         self.item = item
378
379                         disconnect = wx.MenuItem(self, -1, 'Disconnect')
380                         self.AppendItem(disconnect)
381                         self.Bind(wx.EVT_MENU, self.OnDisconnect, id=disconnect.GetId())
382
383                 def OnDisconnect(self, event):
384                         vpn.disconnect(self.item[0])
385
386         def OnContext(self, event):
387                 i = event.GetIndex()
388                 self.PopupMenu(self.ContextMenu(self.list.itemDataMap[event.GetIndex()]), event.GetPosition())
389
390         def refresh(self):
391                 sortstate = self.list.GetSortState()
392                 self.list.itemDataMap = {}
393                 i = 0
394
395                 for key, connection in vpn.connections.items():
396                         if self.list.GetItemCount() <= i:
397                                 self.list.InsertStringItem(i, connection.name)
398                         else:
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)
407                         i += 1
408
409                 while self.list.GetItemCount() > i:
410                         self.list.DeleteItem(self.list.GetItemCount() - 1)
411
412                 self.list.SortListItems(sortstate[0], sortstate[1])
413
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')
434
435                 hbox = wx.BoxSizer(wx.HORIZONTAL)
436                 hbox.Add(self.list, 1, wx.EXPAND)
437                 self.SetSizer(hbox)
438                 self.refresh()
439
440         def refresh(self):
441                 sortstate = self.list.GetSortState()
442                 self.list.itemDataMap = {}
443                 i = 0
444
445                 for key, node in vpn.nodes.items():
446                         if self.list.GetItemCount() <= i:
447                                 self.list.InsertStringItem(i, node.name)
448                         else:
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))
466                         else:
467                                 since = "never"
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)
471                         i += 1
472
473                 while self.list.GetItemCount() > i:
474                         self.list.DeleteItem(self.list.GetItemCount() - 1)
475
476                 self.list.SortListItems(sortstate[0], sortstate[1])
477
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')
488
489                 hbox = wx.BoxSizer(wx.HORIZONTAL)
490                 hbox.Add(self.list, 1, wx.EXPAND)
491                 self.SetSizer(hbox)
492                 self.refresh()
493
494         def refresh(self):
495                 sortstate = self.list.GetSortState()
496                 self.list.itemDataMap = {}
497                 i = 0
498
499                 for key, edge in vpn.edges.items():
500                         if self.list.GetItemCount() <= i:
501                                 self.list.InsertStringItem(i, edge.fr)
502                         else:
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)
511                         i += 1
512
513                 while self.list.GetItemCount() > i:
514                         self.list.DeleteItem(self.list.GetItemCount() - 1)
515
516                 self.list.SortListItems(sortstate[0], sortstate[1])
517
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)
527                 self.SetSizer(hbox)
528                 self.refresh()
529
530         def refresh(self):
531                 sortstate = self.list.GetSortState()
532                 self.list.itemDataMap = {}
533                 i = 0
534
535                 for key, subnet in vpn.subnets.items():
536                         if self.list.GetItemCount() <= i:
537                                 self.list.InsertStringItem(i, subnet.address + '/' + subnet.prefixlen)
538                         else:
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)
544                         i += 1
545
546                 while self.list.GetItemCount() > i:
547                         self.list.DeleteItem(self.list.GetItemCount() - 1)
548
549                 self.list.SortListItems(sortstate[0], sortstate[1])
550
551 class StatusPage(wx.Panel):
552         def __init__(self, parent, id):
553                 wx.Panel.__init__(self, parent, id)
554
555 class GraphPage(wx.Window):
556         def __init__(self, parent, id):
557                 wx.Window.__init__(self, parent, id)
558
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)
569
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')
577                 
578
579 class MainWindow(wx.Frame):
580         def OnQuit(self, event):
581                 app.ExitMainLoop()
582
583         def OnTimer(self, event):
584                 vpn.refresh()
585                 self.np.nodes.refresh()
586                 self.np.subnets.refresh()
587                 self.np.edges.refresh()
588                 self.np.connections.refresh()
589
590         def __init__(self, parent, id, title):
591                 wx.Frame.__init__(self, parent, id, title)
592
593                 menubar = wx.MenuBar()
594                 file = wx.Menu()
595                 file.Append(1, '&Quit\tCtrl-X', 'Quit tinc GUI')
596                 menubar.Append(file, '&File')
597
598                 #nb = wx.Notebook(self, -1)
599                 #nb.SetPadding((0, 0))
600                 self.np = NetPage(self, -1)
601                 #nb.AddPage(np, 'VPN')
602                 
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)
608                 self.Show()
609
610 app = wx.App()
611 mw = MainWindow(None, -1, 'Tinc GUI')
612
613 #def OnTaskBarIcon(event):
614 #       mw.Raise()
615 #
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)
620
621 app.MainLoop()
622 vpn.close()