notify zone changes
[opensuse:fwzs.git] / fwzsd.py
1 #!/usr/bin/python
2 #
3 # fwzsd - fwzs daemon
4 # Copyright (C) 2009 SUSE LINUX Products GmbH
5 #
6 # Author:     Ludwig Nussel
7
8 # This program is free software; you can redistribute it and/or
9 # modify it under the terms of the GNU General Public License
10 # version 2 as published by the Free Software Foundation.
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
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
20
21 import gobject
22
23 import dbus
24 import dbus.service
25 import dbus.mainloop.glib
26 from PolkitAuth import PolkitAuth
27
28 import os
29 import subprocess
30
31 import gettext
32
33 TIMEOUT = 60
34
35 timer = None
36
37 def N_(x): return x
38
39 class FirewallException(dbus.DBusException):
40     _dbus_error_name = 'org.opensuse.zoneswitcher.FirewallException'
41
42 class FirewallNotPrivilegedException(dbus.DBusException):
43     _dbus_error_name = 'org.opensuse.zoneswitcher.FirewallNotPrivilegedException'
44
45 # backends need to implement this class
46 class ZoneSwitcher(gobject.GObject):
47
48     __gsignals__ = {
49         'ZoneChanged':
50             (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,gobject.TYPE_STRING,))
51     }
52
53     def __init__(self, *args):
54         self.__gobject_init__()
55         self.trans = {}
56
57     def Zones(self, sender=None):
58         raise FirewallException("not implemented")
59
60     def Interfaces(self, sender=None):
61         raise FirewallException("not implemented")
62
63     def setZone(self, interface, zone, sender=None):
64         raise FirewallException("not implemented")
65
66     def Run(self, sender=None):
67         raise FirewallException("not implemented")
68
69     def Status(self, sender=None):
70         raise FirewallException("not implemented")
71
72     def setLang(self, lang, sender=None):
73         return self._setlang(lang, sender=sender)
74
75     def _setlang(self, lang, domain="fwzsd", sender=None):
76         # yet another hack. We remember the language per connection
77         try:
78             t = gettext.translation(domain=domain, languages=[lang])
79             self.trans[sender] = t
80             return True
81         except Exception, e:
82             print e
83         return False
84
85     def nameowner_changed_handler(self, name, old, new):
86         if old and not new and old in self.trans:
87             del self.trans[old]
88
89     def do_ZoneChanged(self, iface, zone):
90         return
91
92 class ZoneSwitcherDBUS(dbus.service.Object):
93     """DBUS interface for zone switcher"""
94
95     interface = "org.opensuse.zoneswitcher"
96
97     def __init__(self, impl, *args):
98         dbus.service.Object.__init__(self, *args) 
99         self.impl = impl
100         impl.connect('ZoneChanged', lambda obj, iface, zone: self._zone_changed_receive(iface, zone))
101         self._connection.add_signal_receiver(
102                         lambda name, old, new: self.nameowner_changed_handler(name, old, new),
103                         dbus_interface='org.freedesktop.DBus',
104                         signal_name='NameOwnerChanged')
105         self.mainloop = None
106         self.clients = {}
107
108     def _add_client(self, sender):
109         if (not sender in self.clients):
110             print "add client %s"%sender
111             self.clients[sender] = 1
112             self._update_timeout()
113
114     def _remove_client(self, sender):
115         if (sender in self.clients):
116             print "remove client %s"%sender
117             del self.clients[sender]
118             self._update_timeout()
119
120     def _update_timeout(self,):
121         global timer
122         timer.inhibit("dbusiface", len(self.clients) != 0)
123
124     def set_mainloop(self, l):
125         self.mainloop = l
126
127     @dbus.service.method(interface,
128                          in_signature='', out_signature='a{sa{ss}}', sender_keyword='sender')
129     def Zones(self, sender=None):
130         """Return {"ZONE": "human readable name", ... }"""
131         self._add_client(sender)
132         return self.impl.Zones(sender=sender)
133
134     @dbus.service.method(interface,
135                          in_signature='', out_signature='a{ss}', sender_keyword='sender')
136     def Interfaces(self, sender=None):
137         """Return {"INTERFACENAME": "ZONE", ... }"""
138         self._add_client(sender)
139         return self.impl.Interfaces(sender=sender)
140
141     @dbus.service.method(interface,
142                          in_signature='ss', out_signature='b', sender_keyword='sender', async_callbacks=('return_cb', 'error_cb'))
143     def setZone(self, interface, zone, sender, return_cb, error_cb):
144         """Put the specified interface in the specified zone on next Firewall run
145         Return True|False"""
146         self._add_client(sender)
147         self._check_polkit(sender, "org.opensuse.zoneswitcher.control",
148                 return_cb, error_cb,
149                 lambda interface, zone, sender: self.impl.setZone(interface, zone, sender), interface, zone, sender)
150
151     @dbus.service.method(interface,
152                          in_signature='', out_signature='b', sender_keyword='sender', async_callbacks=('return_cb', 'error_cb'))
153     def Run(self, sender, return_cb, error_cb):
154         """Run the Firewall to apply settings.
155         Return True|False"""
156         self._add_client(sender)
157         self._check_polkit(sender, "org.opensuse.zoneswitcher.control",
158                 return_cb, error_cb,
159                 lambda sender: self.impl.Run(sender), sender)
160
161     @dbus.service.method(interface,
162                          in_signature='', out_signature='b', sender_keyword='sender')
163     def Status(self, sender=None):
164         """Status of backend
165         Return running: True off:False
166         exception on error"""
167         self._add_client(sender)
168         return self.impl.Status(sender=sender)
169
170     @dbus.service.method(interface,
171                          in_signature='s', out_signature='b', sender_keyword='sender')
172     def setLang(self, lang, sender=None):
173         self._add_client(sender)
174         return self.impl.setLang(lang, sender=sender)
175
176     @dbus.service.signal(interface, signature='ss')
177     def ZoneChanged(self, iface, zone):
178         return
179
180     def _zone_changed_receive(self, iface, zone):
181         if not iface:
182             return
183         if not zone:
184             zone = ''
185         print "DBUS: forwarding ZoneChanged(%s, %s)"%(iface, zone)
186         self.ZoneChanged(iface, zone)
187
188     def nameowner_changed_handler(self, name, old, new):
189         if not new and old in self.clients:
190             self._remove_client(old)
191         self.impl.nameowner_changed_handler(name, old, new)
192
193     def _check_polkit(self, sender, action, return_cb, error_cb, func, *args ):
194         #print return_cb, error_cb, func, args
195         pk = PolkitAuth()
196         pk.check(sender, action,
197                 lambda result: self._pk_auth_done(result, return_cb, error_cb, func, *args),
198                 lambda e: self._pk_auth_except(error_cb, e))
199
200     def _pk_auth_done(self, result, return_cb, error_cb, func, *args):
201         #print return_cb, error_cb, func, args
202         r = False
203         if(result):
204             try:
205                 r = func(*args)
206             except Exception, e:
207                 error_cb(e)
208                 return
209         else:
210                 error_cb(FirewallException(N_("You are not authorized.")))
211                 
212         return_cb(r)
213
214     def _pk_auth_except(self, error_cb, e):
215         error_cb(e)
216
217 class ZoneSwitcherSuSEfirewall2(ZoneSwitcher):
218
219     ZONES = {
220         'int': N_('Trusted'),
221         'dmz': N_('DMZ'),
222         'ext': N_('Untrusted'),
223     }
224
225     STATUSDIR = '/var/run/SuSEfirewall2/status'
226     IFACEOVERRIDEDIR = '/var/run/SuSEfirewall2/override/interfaces'
227
228     def _listzones(self):
229         try:
230             return os.listdir(self.STATUSDIR + '/zones')
231         except:
232             return []
233
234     def _listiterfaces(self):
235         try:
236             return os.listdir(self.STATUSDIR + '/interfaces')
237         except:
238             return []
239
240     def Zones(self, sender=None):
241         ret = {}
242
243         for z in self._listzones():
244             ret[z] = { 'desc' : '' }
245             if z in self.ZONES:
246                 ret[z]['desc'] = self.ZONES[z]
247                 if sender in self.trans:
248                     ret[z]['desc'] = self.trans[sender].gettext(ret[z]['desc'])
249
250         return ret
251
252     def Interfaces(self, sender=None):
253         ret = {}
254
255         for i in self._listiterfaces():
256             ret[i] = self._get_zone(i)
257         return ret
258
259     def _get_zone(self, interface):
260         try:
261             f = open(self.STATUSDIR+'/interfaces/'+interface+'/zone')
262             z = f.readline()
263             return z[:len(z)-1]
264         except:
265             return ""
266
267     def setZone(self, interface, zone, sender=None):
268         # check user supplied strings
269         if not interface in self._listiterfaces():
270             raise FirewallException("specified interface is invalid")
271         if zone and not zone in self._listzones():
272             raise FirewallException("specified zone is invalid")
273
274         dir = self.IFACEOVERRIDEDIR+'/'+interface
275         if not os.access(dir, os.F_OK):
276             os.makedirs(dir)
277         file = dir+'/zone'
278         if (zone):
279             f = open(file, 'w')
280             print >>f, zone
281             f.close()
282         else:
283             if os.access(file, os.F_OK):
284                 os.unlink(file)
285
286         self.emit("ZoneChanged", interface, zone)
287         return True
288
289     def Run(self, sender=None):
290         try:
291             if(subprocess.call(['/sbin/SuSEfirewall2']) != 0):
292                 raise FirewallException("SuSEfirewall2 failed")
293         except:
294             raise FirewallException("can't run SuSEfirewall2")
295         return True
296
297     def Status(self, sender=None):
298         try:
299             #n = open('/dev/null', 'w')
300             #if(subprocess.call(['/sbin/SuSEfirewall2', 'status'], stdout=n, stderr=n) == 0):
301             if (os.access(self.STATUSDIR, os.F_OK)):
302                 return True
303             return False
304         except Exception, e:
305             print e
306             raise FirewallException("SuSEfirewall2 status unknown")
307
308
309 class NMWatcher:
310
311     DEVSTATES = {
312               0: 'UNKNOWN',
313              10: 'UNMANAGED',
314              20: 'UNAVAILABLE',
315              30: 'DISCONNECTED',
316              40: 'PREPARE',
317              50: 'CONFIG',
318              60: 'NEED_AUTH',
319              70: 'IP_CONFIG',
320              80: 'IP_CHECK',
321              90: 'SECONDARIES',
322             100: 'ACTIVATED',
323             110: 'DEACTIVATING',
324             120: 'FAILED',
325             }
326
327     STATEDIR = "/var/lib/zoneswitcher"
328
329     def __init__(self, switcher):
330         self.bus = dbus.SystemBus()
331         self.proxy = None
332         self.manager = None
333         self.running = False
334         self.devuuid = {} # devname => uuid
335         self.zones = {} # uuid => zone
336         self.switcher = switcher
337         self.devicewatchers = {}
338
339         self.readstate()
340
341         self.check_status()
342
343         self.bus.add_signal_receiver(
344             lambda name, old, new: self.nameowner_changed_handler(name, old, new),
345                 dbus_interface='org.freedesktop.DBus',
346                 signal_name='NameOwnerChanged')
347
348         self.bus.add_signal_receiver(
349             lambda device, **kwargs: self.device_add_rm(device, True, **kwargs),
350                 dbus_interface = 'org.freedesktop.NetworkManager',
351                 signal_name = 'DeviceAdded',
352                 sender_keyword = 'sender')
353
354         self.bus.add_signal_receiver(
355             lambda device, **kwargs: self.device_add_rm(device, False, **kwargs),
356                 dbus_interface = 'org.freedesktop.NetworkManager',
357                 signal_name = 'DeviceRemoved',
358                 sender_keyword = 'sender')
359
360         if not os.access(self.STATEDIR, os.F_OK):
361             os.makedirs(self.STATEDIR)
362
363     def cleanup(self):
364         self.switcher = None
365
366     def savestate(self):
367         print "save state"
368         file = self.STATEDIR + "/nmwatcher.zones"
369         f = open(file, 'w')
370         for uuid in self.zones.keys():
371             print >>f, "%s %s"%(uuid, self.zones[uuid])
372         f.close()
373
374     def readstate(self):
375         print "read state"
376         file = self.STATEDIR + "/nmwatcher.zones"
377         if not os.access(file, os.F_OK):
378             return
379         f = open(file, 'r')
380         if (f):
381             line = f.readline()
382             while(line):
383                 a = line.split('\n')[0].split(' ')
384                 if (len(a) == 2):
385                     print "%s -> %s"%(a[0], a[1])
386                     self.zones[a[0]] = a[1]
387                 line = f.readline()
388             f.close()
389
390     def devstate2name(self, state):
391         if state in self.DEVSTATES:
392             return self.DEVSTATES[state]
393         return "UNKNOWN:%s"%state
394
395     def nameowner_changed_handler(self, name, old, new):
396         if name != 'org.freedesktop.NetworkManager':
397             return
398         
399         off = old and not new
400         self.check_status(off)
401
402     def device_add_rm(self, device, added, sender=None, **kwargs):
403         if (added):
404             self.watch_device(device)
405         else:
406             print "device %s removed"%device
407             if (device in self.devicewatchers):
408                 self.devicewatchers[device].remove()
409                 del self.devicewatchers[device]
410
411     def device_state_changed_handler(self, props, name, new, old, reason, **kwargs):
412         uuid = None
413         try:
414             conn_path = props.Get("org.freedesktop.NetworkManager.Device", "ActiveConnection")
415             uuid = self.activeconn_get_uuid(conn_path)
416         except dbus.DBusException, e:
417             pass
418         print "%s: state change %s -> %s" % (name, self.devstate2name(old), self.devstate2name(new))
419         needchange = False
420         if (not name in self.devuuid):
421             print "%s: new uuid %s"%(name, uuid)
422             needchange = True
423         elif (self.devuuid[name] != uuid):
424             print "%s: uuid change %s -> %s"%(name, self.devuuid[name], uuid)
425             needchange = True
426             if (not uuid): # device went down, save previously used zone
427                 self.check_and_save(name, self.devuuid[name])
428
429         if (needchange):
430             try:
431                 z = None
432                 if (uuid and uuid in self.zones):
433                     z = self.zones[uuid]
434                 print "%s: setting zone to %s"%(name, z)
435                 self.switcher.setZone(name, z)
436                 if (self.switcher.Status()):
437                     self.switcher.Run()
438             except FirewallException, e:
439                 print e
440
441             if (uuid):
442                 self.check_and_save(name, uuid)
443
444             self.devuuid[name] = uuid
445
446     def check_and_save(self, name, uuid):
447             z = self.switcher._get_zone(name)
448             if (z == ""):
449                 z = None
450             if (z and (not uuid in self.zones or self.zones[uuid] != z)):
451                 print "%s: new zone %s"%(uuid, z)
452                 self.zones[uuid] = z
453                 self.savestate()
454
455     def _connect_nm(self):
456         try:
457             self.proxy = self.bus.get_object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
458             self.manager = manager = dbus.Interface(self.proxy, "org.freedesktop.NetworkManager")
459             running = True
460         except dbus.DBusException, e:
461             running = False
462
463         return running
464
465     def check_status(self, force_off=False):
466         if (force_off):
467             running = False
468         else:
469             running = self.running
470             if (not self.manager):
471                 running = self._connect_nm()
472
473         if (running):
474             if (not self.running):
475                 devices = self.manager.GetDevices()
476                 for d in devices:
477                     self.watch_device(d)
478
479         if (not running):
480             self.proxy = self.manager = None
481             self.devices = None
482             for d in self.devicewatchers:
483                 self.devicewatchers[d].remove()
484             self.devicewatchers = {}
485
486         self.running = running
487         print "NM Running: ", self.running
488         global timer
489         timer.inhibit("nm", running)
490         return
491
492     def activeconn_get_uuid(self, path):
493         try:
494             if (path != '/'):
495                 conn = self.bus.get_object("org.freedesktop.NetworkManager", path)
496                 return conn.Get( "org.freedesktop.NetworkManager.Connection.Active", "Uuid",
497                         dbus_interface="org.freedesktop.DBus.Properties")
498         except dbus.DBusException, e:
499             pass
500         return None
501
502     def watch_device(self, d):
503         # already watched. could happen if NM restarts and we both query all
504         # devices and receive device add signals.
505         if (d in self.devicewatchers):
506             return
507
508         dev = self.bus.get_object("org.freedesktop.NetworkManager", d)
509         props = dbus.Interface(dev, "org.freedesktop.DBus.Properties")
510         name = props.Get("org.freedesktop.NetworkManager.Device", "Interface")
511         state = props.Get("org.freedesktop.NetworkManager.Device", "State")
512         conn_path = props.Get("org.freedesktop.NetworkManager.Device", "ActiveConnection")
513
514         uuid = self.activeconn_get_uuid(conn_path)
515
516         self.devuuid[name] = uuid
517
518         print "Watching %s, state %s, uuid %s" % (name, self.devstate2name(state), uuid)
519         self.devicewatchers[d] = self.bus.add_signal_receiver(
520                 lambda new, old, reason, **kwargs: self.device_state_changed_handler(props, name, new, old, reason, **kwargs),
521                     dbus_interface = 'org.freedesktop.NetworkManager.Device',
522                     signal_name = 'StateChanged',
523                     path = d, sender_keyword = 'sender')
524         ## XXX: not sure why setZone was needed here:
525 #       try:
526 #           self.switcher.setZone(name, None)
527 #       except FirewallException, e:
528 #           print e
529
530 class Timer:
531
532     def __init__(self, mainloop):
533         self.timeout = None
534         self.mainloop = mainloop
535         self.inhibitors = {}
536
537         self._start()
538
539     def inhibit(self, who, doit):
540         if (doit):
541             self.inhibitors[who] = 1
542             print "inhibitor %s added"%who
543         elif (who in self.inhibitors):
544             del self.inhibitors[who]
545             print "inhibitor %s removed"%who
546
547         if len(self.inhibitors) == 0:
548             self._start()
549         else:
550             if self.timeout:
551                 gobject.source_remove(self.timeout)
552                 print "timer deleted"
553
554     def _start(self):
555             if self.timeout:
556                 gobject.source_remove(self.timeout)
557             self.timeout = gobject.timeout_add(TIMEOUT * 1000, self._goodbye)
558             print "new timer installed"
559
560     def _goodbye(self):
561         if len(self.inhibitors):
562             print "inhibitors != 0. Should not happen!", self.inhibitors
563             return True
564         print "exit due to timeout"
565         self.mainloop.quit()
566         return False
567
568 if __name__ == '__main__':
569     dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
570     mainloop = gobject.MainLoop()
571
572     timer = Timer(mainloop)
573
574     bus = dbus.SystemBus()
575     name = dbus.service.BusName("org.opensuse.zoneswitcher", bus)
576     switcher = ZoneSwitcherSuSEfirewall2()
577     object = ZoneSwitcherDBUS(switcher, bus, '/org/opensuse/zoneswitcher0')
578
579     nm = NMWatcher(switcher)
580
581     object.set_mainloop(mainloop)
582     mainloop.run()
583
584 # vim: sw=4 ts=8 noet