Pavel Šimerda 28bc5fe
diff --git a/dnssec-trigger-script.in b/dnssec-trigger-script.in
Pavel Šimerda c6b3534
index b572dd1..b25afc9 100644
Pavel Šimerda 28bc5fe
--- a/dnssec-trigger-script.in
Pavel Šimerda 28bc5fe
+++ b/dnssec-trigger-script.in
Pavel Šimerda c6b3534
@@ -6,17 +6,20 @@
Pavel Šimerda 654266c
 """
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
 from gi.repository import NMClient
Pavel Šimerda 654266c
-import os, sys, shutil, subprocess
Pavel Šimerda 406c48d
+import os, sys, fcntl, shutil, glob, subprocess
Pavel Šimerda 28bc5fe
 import logging, logging.handlers
Pavel Šimerda 28bc5fe
 import socket, struct
Pavel Šimerda 28bc5fe
 
Pavel Šimerda c6b3534
+# Python compatibility stuff
Pavel Šimerda c6b3534
+if not hasattr(os, "O_CLOEXEC"):
Pavel Šimerda c6b3534
+    os.O_CLOEXEC = 0x80000
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
 DEVNULL = open("/dev/null", "wb")
Pavel Šimerda c6b3534
 
Pavel Šimerda 28bc5fe
 log = logging.getLogger()
Pavel Šimerda 654266c
 log.setLevel(logging.INFO)
Pavel Šimerda 654266c
 log.addHandler(logging.handlers.SysLogHandler())
Pavel Šimerda 654266c
-if sys.stderr.isatty():
Pavel Šimerda 654266c
-    log.addHandler(logging.StreamHandler())
Pavel Šimerda 654266c
+log.addHandler(logging.StreamHandler())
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
 # NetworkManager reportedly doesn't pass the PATH environment variable.
Pavel Šimerda 654266c
 os.environ['PATH'] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Pavel Šimerda c6b3534
@@ -24,12 +27,37 @@ os.environ['PATH'] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/b
Pavel Šimerda 406c48d
 class UserError(Exception):
Pavel Šimerda 406c48d
     pass
Pavel Šimerda 406c48d
 
Pavel Šimerda 406c48d
+class Lock:
Pavel Šimerda 406c48d
+    """Lock used to serialize the script"""
Pavel Šimerda 406c48d
+
Pavel Šimerda 406c48d
+    path = "/var/run/dnssec-trigger/lock"
Pavel Šimerda 406c48d
+
Pavel Šimerda 406c48d
+    def __init__(self):
Pavel Šimerda 406c48d
+        # We don't use os.makedirs(..., exist_ok=True) to ensure Python 2 compatibility
Pavel Šimerda 406c48d
+        dirname = os.path.dirname(self.path)
Pavel Šimerda 406c48d
+        if not os.path.exists(dirname):
Pavel Šimerda 406c48d
+            os.makedirs(dirname)
Pavel Šimerda c6b3534
+        self.lock = os.open(self.path, os.O_WRONLY | os.O_CREAT | os.O_CLOEXEC, 0o600)
Pavel Šimerda 406c48d
+
Pavel Šimerda 406c48d
+    def __enter__(self):
Pavel Šimerda 406c48d
+        fcntl.lockf(self.lock, fcntl.LOCK_EX)
Pavel Šimerda 406c48d
+
Pavel Šimerda 406c48d
+    def __exit__(self, t, v, tb):
Pavel Šimerda 406c48d
+        fcntl.lockf(self.lock, fcntl.LOCK_UN)
Pavel Šimerda 406c48d
+
Pavel Šimerda 406c48d
 class Config:
Pavel Šimerda 406c48d
     """Global configuration options"""
Pavel Šimerda 406c48d
 
Pavel Šimerda c6b3534
     path = "/etc/dnssec.conf"
Pavel Šimerda c6b3534
-    validate_connection_provided_zones = True
Pavel Šimerda c6b3534
-    add_wifi_provided_zones = False
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    bool_options = {
Pavel Šimerda c6b3534
+        "debug": False,
Pavel Šimerda c6b3534
+        "validate_connection_provided_zones": True,
Pavel Šimerda c6b3534
+        "add_wifi_provided_zones": False,
Pavel Šimerda c6b3534
+        "use_vpn_global_forwarders": False,
Pavel Šimerda c6b3534
+        "use_resolv_conf_symlink": False,
Pavel Šimerda c6b3534
+        "use_resolv_secure_conf_symlink": False,
Pavel Šimerda c6b3534
+    }
Pavel Šimerda c6b3534
 
Pavel Šimerda c6b3534
     def __init__(self):
Pavel Šimerda c6b3534
         try:
Pavel Šimerda c6b3534
@@ -37,35 +65,36 @@ class Config:
Pavel Šimerda c6b3534
                 for line in config_file:
Pavel Šimerda c6b3534
                     if '=' in line:
Pavel Šimerda c6b3534
                         option, value = [part.strip() for part in line.split("=", 1)]
Pavel Šimerda c6b3534
-                        if option == "validate_connection_provided_zones":
Pavel Šimerda c6b3534
-                            self.validate_connection_provided_zones = (value == "yes")
Pavel Šimerda c6b3534
-                        elif option == "add_wifi_provided_zones":
Pavel Šimerda c6b3534
-                            self.add_wifi_provided_zones = (value == "yes")
Pavel Šimerda c6b3534
+                        if option in self.bool_options:
Pavel Šimerda c6b3534
+                            self.bool_options[option] = (value == "yes")
Pavel Šimerda c6b3534
         except IOError:
Pavel Šimerda c6b3534
             pass
Pavel Šimerda c6b3534
         log.debug(self)
Pavel Šimerda c6b3534
 
Pavel Šimerda c6b3534
-    def __repr__(self):
Pavel Šimerda c6b3534
-        return "<Config validate_connection_provided_zones={validate_connection_provided_zones} add_wifi_provided_zones={add_wifi_provided_zones}>".format(**vars(self))
Pavel Šimerda c6b3534
+    def __getattr__(self, option):
Pavel Šimerda c6b3534
+        return self.bool_options[option]
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    def __str__(self):
Pavel Šimerda c6b3534
+        return "<Config {}>".format(self.bool_options)
Pavel Šimerda c6b3534
 
Pavel Šimerda c6b3534
 class ConnectionList:
Pavel Šimerda c6b3534
     """List of NetworkManager active connections"""
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
     nm_connections = None
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
-    def __init__(self, only_default=False, skip_wifi=False):
Pavel Šimerda c6b3534
+    def __init__(self, client, only_default=False, only_vpn=False, skip_wifi=False):
Pavel Šimerda 654266c
         # Cache the active connection list in the class
Pavel Šimerda d1568a2
+        if not client.get_manager_running():
Pavel Šimerda d1568a2
+            raise UserError("NetworkManager is not running.")
Pavel Šimerda 654266c
         if self.nm_connections is None:
Pavel Šimerda 654266c
-            self.__class__.client = NMClient.Client()
Pavel Šimerda 654266c
-            self.__class__.nm_connections = self.client.get_active_connections()
Pavel Šimerda 654266c
+            self.__class__.nm_connections = client.get_active_connections()
Pavel Šimerda 654266c
         self.skip_wifi = skip_wifi
Pavel Šimerda 654266c
         self.only_default = only_default
Pavel Šimerda c6b3534
+        self.only_vpn = only_vpn
Pavel Šimerda 654266c
         log.debug(self)
Pavel Šimerda d1568a2
 
Pavel Šimerda d1568a2
     def __repr__(self):
Pavel Šimerda d1568a2
-        if not list(self):
Pavel Šimerda d1568a2
-            raise Exception("!!!")
Pavel Šimerda c6b3534
-        return "<ConnectionList(only_default={only_default}, skip_wifi={skip_wifi}, connections={})>".format(list(self), **vars(self))
Pavel Šimerda c6b3534
+        return "<ConnectionList(only_default={only_default}, only_vpn={only_vpn}, skip_wifi={skip_wifi}, connections={})>".format(list(self), **vars(self))
Pavel Šimerda d1568a2
 
Pavel Šimerda d1568a2
     def __iter__(self):
Pavel Šimerda c6b3534
         for item in self.nm_connections:
Pavel Šimerda c6b3534
@@ -82,6 +111,8 @@ class ConnectionList:
Pavel Šimerda c6b3534
             # Skip non-default connections if appropriate
Pavel Šimerda c6b3534
             if self.only_default and not connection.is_default:
Pavel Šimerda c6b3534
                 continue
Pavel Šimerda c6b3534
+            if self.only_vpn and not connection.is_vpn:
Pavel Šimerda c6b3534
+                continue
Pavel Šimerda c6b3534
             yield connection
Pavel Šimerda c6b3534
 
Pavel Šimerda c6b3534
     def get_zone_connection_mapping(self):
Pavel Šimerda c6b3534
@@ -190,10 +221,10 @@ class UnboundZoneConfig:
Pavel Šimerda 28bc5fe
                 if fields.pop(0) in ('forward', 'forward:'):
Pavel Šimerda 28bc5fe
                     fields.pop(0)
Pavel Šimerda 28bc5fe
                 secure = False
Pavel Šimerda 28bc5fe
-                if fields[0] == '+i':
Pavel Šimerda 28bc5fe
+                if fields and fields[0] == '+i':
Pavel Šimerda 28bc5fe
                     secure = True
Pavel Šimerda 28bc5fe
                     fields.pop(0)
Pavel Šimerda 406c48d
-                self.cache[name] = set(fields[3:]), secure
Pavel Šimerda 406c48d
+                self.cache[name] = set(fields), secure
Pavel Šimerda 406c48d
         log.debug(self)
Pavel Šimerda 406c48d
 
Pavel Šimerda 406c48d
     def __repr__(self):
Pavel Šimerda c6b3534
@@ -255,7 +286,7 @@ class Store:
Pavel Šimerda 28bc5fe
                     line = line.strip()
Pavel Šimerda 28bc5fe
                     if line:
Pavel Šimerda 28bc5fe
                         self.cache.add(line)
Pavel Šimerda 28bc5fe
-        except FileNotFoundError:
Pavel Šimerda 28bc5fe
+        except IOError:
Pavel Šimerda 28bc5fe
             pass
Pavel Šimerda 28bc5fe
         log.debug(self)
Pavel Šimerda 28bc5fe
 
Pavel Šimerda c6b3534
@@ -277,10 +308,16 @@ class Store:
Pavel Šimerda 654266c
         log.debug(self)
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
     def update(self, zones):
Pavel Šimerda 654266c
-        """Commit a new zone list."""
Pavel Šimerda 654266c
+        """Commit a new set of items and return True when it differs"""
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
-        self.cache = set(zones)
Pavel Šimerda 654266c
-        log.debug(self)
Pavel Šimerda 654266c
+        zones = set(zones)
Pavel Šimerda 654266c
+
Pavel Šimerda 654266c
+        if zones != self.cache:
Pavel Šimerda 654266c
+            self.cache = set(zones)
Pavel Šimerda 654266c
+            log.debug(self)
Pavel Šimerda 654266c
+            return True
Pavel Šimerda 654266c
+
Pavel Šimerda 654266c
+        return False
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
     def remove(self, zone):
Pavel Šimerda 654266c
         """Remove zone from the cache."""
Pavel Šimerda c6b3534
@@ -309,10 +346,21 @@ class GlobalForwarders:
Pavel Šimerda 28bc5fe
                     line = line.strip()
Pavel Šimerda 28bc5fe
                     if line:
Pavel Šimerda 28bc5fe
                         self.cache.add(line)
Pavel Šimerda 28bc5fe
-        except FileNotFoundError:
Pavel Šimerda 28bc5fe
+        except IOError:
Pavel Šimerda 28bc5fe
             pass
Pavel Šimerda 28bc5fe
 
Pavel Šimerda 28bc5fe
 class Application:
Pavel Šimerda c6b3534
+    resolvconf = "/etc/resolv.conf"
Pavel Šimerda c6b3534
+    resolvconf_tmp = "/etc/.resolv.conf.dnssec-trigger"
Pavel Šimerda c6b3534
+    resolvconf_secure = "/etc/resolv-secure.conf"
Pavel Šimerda c6b3534
+    resolvconf_secure_tmp = "/etc/.resolv-secure.conf.dnssec-trigger"
Pavel Šimerda c6b3534
+    resolvconf_backup = "/var/run/dnssec-trigger/resolv.conf.backup"
Pavel Šimerda c6b3534
+    resolvconf_trigger = "/var/run/dnssec-trigger/resolv.conf"
Pavel Šimerda c6b3534
+    resolvconf_trigger_tmp = resolvconf_trigger + ".tmp"
Pavel Šimerda c6b3534
+    resolvconf_networkmanager = "/var/run/NetworkManager/resolv.conf"
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    resolvconf_localhost_contents = "# Generated by dnssec-trigger-script\nnameserver 127.0.0.1\n"
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
     def __init__(self, argv):
Pavel Šimerda c6b3534
         if len(argv) > 1 and argv[1] == '--debug':
Pavel Šimerda c6b3534
             argv.pop(1)
Pavel Šimerda c6b3534
@@ -327,108 +375,222 @@ class Application:
Pavel Šimerda c6b3534
             self.method = getattr(self, "run_" + argv[1][2:].replace('-', '_'))
Pavel Šimerda 654266c
         except AttributeError:
Pavel Šimerda 654266c
             self.usage()
Pavel Šimerda c6b3534
+
Pavel Šimerda 654266c
         self.config = Config()
Pavel Šimerda c6b3534
+        if self.config.debug:
Pavel Šimerda c6b3534
+            log.setLevel(logging.DEBUG);
Pavel Šimerda 654266c
+
Pavel Šimerda c6b3534
+        self.client = NMClient.Client()
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
     def nm_handles_resolv_conf(self):
Pavel Šimerda 654266c
-        if subprocess.call(["pidof", "NetworkManager"], stdout=DEVNULL, stderr=DEVNULL) != 0:
Pavel Šimerda 654266c
+        if not self.client.get_manager_running():
Pavel Šimerda 654266c
+            log.debug("NetworkManager is not running")
Pavel Šimerda 654266c
             return False
Pavel Šimerda 28bc5fe
         try:
Pavel Šimerda 28bc5fe
             with open("/etc/NetworkManager/NetworkManager.conf") as nm_config_file:
Pavel Šimerda 28bc5fe
                 for line in nm_config_file:
Pavel Šimerda 28bc5fe
-                    if line.strip == "dns=none":
Pavel Šimerda 28bc5fe
+                    if line.strip() in ("dns=none", "dns=unbound"):
Pavel Šimerda c6b3534
+                        log.debug("NetworkManager doesn't handle resolv.conf")
Pavel Šimerda 28bc5fe
                         return False
Pavel Šimerda 28bc5fe
         except IOError:
Pavel Šimerda 28bc5fe
             pass
Pavel Šimerda c6b3534
+        log.debug("NetworkManager handles resolv.conf")
Pavel Šimerda 654266c
         return True
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
     def usage(self):
Pavel Šimerda c6b3534
-        raise UserError("Usage: dnssec-trigger-script [--debug] [--async] --prepare|--update|--update-global-forwarders|--update-connection-zones|--cleanup")
Pavel Šimerda c6b3534
+        raise UserError("Usage: dnssec-trigger-script [--debug] [--async] --prepare|--setup|--update|--update-global-forwarders|--update-connection-zones|--cleanup")
Pavel Šimerda 91491c3
 
Pavel Šimerda 91491c3
     def run(self):
Pavel Šimerda c6b3534
         log.debug("Running: {}".format(self.method.__name__))
Pavel Šimerda c6b3534
         self.method()
Pavel Šimerda 91491c3
 
Pavel Šimerda c6b3534
+    def _check_resolv_conf(self, path):
Pavel Šimerda c6b3534
+        try:
Pavel Šimerda c6b3534
+            with open(path) as source:
Pavel Šimerda c6b3534
+                if source.read() != self.resolvconf_localhost_contents:
Pavel Šimerda c6b3534
+                    log.warning("Detected incorrect contents of {!r}!".format(path))
Pavel Šimerda c6b3534
+                    return False;
Pavel Šimerda c6b3534
+                return True
Pavel Šimerda c6b3534
+        except IOError:
Pavel Šimerda c6b3534
+            return False
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    def _write_resolv_conf(self, path):
Pavel Šimerda c6b3534
+        self._try_remove(path)
Pavel Šimerda c6b3534
+        with open(path, "w") as target:
Pavel Šimerda c6b3534
+            target.write(self.resolvconf_localhost_contents)
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    def _install_resolv_conf(self, path, path_tmp, symlink=False):
Pavel Šimerda c6b3534
+        if symlink:
Pavel Šimerda c6b3534
+            self._try_remove(path_tmp)
Pavel Šimerda c6b3534
+            os.symlink(self.resolvconf_trigger, path_tmp)
Pavel Šimerda c6b3534
+            self._try_set_mutable(path)
Pavel Šimerda c6b3534
+            os.rename(path_tmp, path)
Pavel Šimerda c6b3534
+        elif not self._check_resolv_conf(path):
Pavel Šimerda c6b3534
+            self._write_resolv_conf(path_tmp)
Pavel Šimerda c6b3534
+            self._try_set_mutable(path)
Pavel Šimerda c6b3534
+            os.rename(path_tmp, path)
Pavel Šimerda c6b3534
+            self._try_set_immutable(path)
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    def _try_remove(self, path):
Pavel Šimerda c6b3534
+        self._try_set_mutable(path)
Pavel Šimerda c6b3534
+        try:
Pavel Šimerda c6b3534
+            os.remove(path)
Pavel Šimerda c6b3534
+        except OSError:
Pavel Šimerda c6b3534
+            pass
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    def _try_set_immutable(self, path):
Pavel Šimerda c6b3534
+        subprocess.call(["chattr", "+i", path])
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    def _try_set_mutable(self, path):
Pavel Šimerda c6b3534
+        if os.path.exists(path) and not os.path.islink(path):
Pavel Šimerda c6b3534
+            subprocess.call(["chattr", "-i", path])
Pavel Šimerda c6b3534
+
Pavel Šimerda 91491c3
     def run_prepare(self):
Pavel Šimerda c6b3534
-        """Prepare for dnssec-trigger."""
Pavel Šimerda c6b3534
+        """Prepare for starting dnssec-trigger
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+        Called by the service manager before starting dnssec-trigger daemon.
Pavel Šimerda c6b3534
+        """
Pavel Šimerda 654266c
 
Pavel Šimerda c6b3534
+        # Backup resolv.conf when appropriate
Pavel Šimerda 654266c
         if not self.nm_handles_resolv_conf():
Pavel Šimerda c6b3534
-            log.info("Backing up /etc/resolv.conf")
Pavel Šimerda 654266c
-            shutil.copy("/etc/resolv.conf", "/var/run/dnssec-trigger/resolv.conf.bak")
Pavel Šimerda c6b3534
+            try:
Pavel Šimerda c6b3534
+                log.info("Backing up {} as {}...".format(self.resolvconf, self.resolvconf_backup))
Pavel Šimerda c6b3534
+                shutil.move(self.resolvconf, self.resolvconf_backup)
Pavel Šimerda c6b3534
+            except IOError as error:
Pavel Šimerda c6b3534
+                log.warning("Cannot back up {!r} as {!r}: {}".format(self.resolvconf, self.resolvconf_backup, error.strerror))
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+        # Make sure dnssec-trigger daemon doesn't get confused by existing files.
Pavel Šimerda c6b3534
+        self._try_remove(self.resolvconf)
Pavel Šimerda c6b3534
+        self._try_remove(self.resolvconf_secure)
Pavel Šimerda c6b3534
+        self._try_remove(self.resolvconf_trigger)
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    def run_setup(self):
Pavel Šimerda c6b3534
+        """Set up resolv.conf with localhost nameserver
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+        Called by dnssec-trigger.
Pavel Šimerda c6b3534
+        """
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+        self._install_resolv_conf(self.resolvconf_trigger, self.resolvconf_trigger_tmp, False)
Pavel Šimerda c6b3534
+        self._install_resolv_conf(self.resolvconf, self.resolvconf_tmp, self.config.use_resolv_conf_symlink)
Pavel Šimerda c6b3534
+        self._install_resolv_conf(self.resolvconf_secure, self.resolvconf_secure_tmp, self.config.use_resolv_secure_conf_symlink)
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+    def run_restore(self):
Pavel Šimerda c6b3534
+        """Restore resolv.conf with original data
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+        Called by dnssec-trigger or internally as part of other actions.
Pavel Šimerda c6b3534
+        """
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+        self._try_remove(self.resolvconf)
Pavel Šimerda c6b3534
+        self._try_remove(self.resolvconf_secure)
Pavel Šimerda c6b3534
+        self._try_remove(self.resolvconf_trigger)
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+        log.info("Recovering {}...".format(self.resolvconf))
Pavel Šimerda c6b3534
+        if self.nm_handles_resolv_conf():
Pavel Šimerda c6b3534
+            if os.path.isfile(self.resolvconf_networkmanager):
Pavel Šimerda c6b3534
+                os.symlink(self.resolvconf_networkmanager, self.resolvconf)
Pavel Šimerda c6b3534
+            elif os.path.isfile("/sys/fs/cgroup/systemd"):
Pavel Šimerda c6b3534
+                subprocess.check_call(["systemctl", "--ignore-dependencies", "try-restart", "NetworkManager.service"])
Pavel Šimerda c6b3534
+            else:
Pavel Šimerda c6b3534
+                subprocess.check_call(["/etc/init.d/NetworkManager", "restart"])
Pavel Šimerda c6b3534
+        else:
Pavel Šimerda c6b3534
+            try:
Pavel Šimerda c6b3534
+                shutil.move(self.resolvconf_backup, self.resolvconf)
Pavel Šimerda c6b3534
+            except IOError as error:
Pavel Šimerda c6b3534
+                log.warning("Cannot restore {!r} from {!r}: {}".format(self.resolvconf, self.resolvconf_backup, error.strerror))
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
     def run_cleanup(self):
Pavel Šimerda c6b3534
-        """Clean up after dnssec-trigger."""
Pavel Šimerda c6b3534
+        """Clean up after dnssec-trigger daemon
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+        Called by the service manager after stopping dnssec-trigger daemon.
Pavel Šimerda c6b3534
+        """
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+        self.run_restore()
Pavel Šimerda c6b3534
 
Pavel Šimerda d1568a2
         stored_zones = Store('zones')
Pavel Šimerda c6b3534
+        stored_servers = Store('servers')
Pavel Šimerda d1568a2
         unbound_zones = UnboundZoneConfig()
Pavel Šimerda d1568a2
 
Pavel Šimerda d1568a2
+        # provide upgrade path for previous versions
Pavel Šimerda d1568a2
+        old_zones = glob.glob("/var/run/dnssec-trigger/????????-????-????-????-????????????")
Pavel Šimerda d1568a2
+        if old_zones:
Pavel Šimerda d1568a2
+            log.info("Reading zones from the legacy zone store")
Pavel Šimerda d1568a2
+            with open("/var/run/dnssec-trigger/zones", "a") as target:
Pavel Šimerda d1568a2
+                for filename in old_zones:
Pavel Šimerda d1568a2
+                    with open(filename) as source:
Pavel Šimerda d1568a2
+                        log.debug("Reading zones from {}".format(filename))
Pavel Šimerda d1568a2
+                        for line in source:
Pavel Šimerda d1568a2
+                            stored_zones.add(line.strip())
Pavel Šimerda d1568a2
+                        os.remove(filename)
Pavel Šimerda d1568a2
+
Pavel Šimerda d1568a2
         log.debug("clearing unbound configuration")
Pavel Šimerda d1568a2
         for zone in stored_zones:
Pavel Šimerda d1568a2
             unbound_zones.remove(zone)
Pavel Šimerda c6b3534
             stored_zones.remove(zone)
Pavel Šimerda c6b3534
+        for server in stored_servers:
Pavel Šimerda c6b3534
+            stored_servers.remove(server)
Pavel Šimerda c6b3534
         stored_zones.commit()
Pavel Šimerda c6b3534
-
Pavel Šimerda c6b3534
-        log.debug("recovering /etc/resolv.conf")
Pavel Šimerda c6b3534
-        subprocess.check_call(["chattr", "-i", "/etc/resolv.conf"])
Pavel Šimerda c6b3534
-        if not self.nm_handles_resolv_conf():
Pavel Šimerda 654266c
-            shutil.copy("/var/run/dnssec-trigger/resolv.conf.bak", "/etc/resolv.conf")
Pavel Šimerda c6b3534
-        # NetworkManager currently doesn't support explicit /etc/resolv.conf
Pavel Šimerda c6b3534
-        # write out. For now we simply restart the daemon.
Pavel Šimerda c6b3534
-        elif os.path.exists("/sys/fs/cgroup/systemd"):
Pavel Šimerda 2073a47
-            subprocess.check_call(["systemctl", "try-restart", "NetworkManager.service"])
Pavel Šimerda c6b3534
-        else:
Pavel Šimerda c6b3534
-            subprocess.check_call(["/etc/init.d/NetworkManager", "restart"])
Pavel Šimerda c6b3534
+        stored_servers.commit()
Pavel Šimerda 2073a47
 
Pavel Šimerda c6b3534
     def run_update(self):
Pavel Šimerda c6b3534
+        """Update unbound and dnssec-trigger configuration."""
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
         self.run_update_global_forwarders()
Pavel Šimerda c6b3534
         self.run_update_connection_zones()
Pavel Šimerda 654266c
 
Pavel Šimerda c6b3534
     def run_update_global_forwarders(self):
Pavel Šimerda c6b3534
         """Configure global forwarders using dnssec-trigger-control."""
Pavel Šimerda c6b3534
 
Pavel Šimerda c6b3534
-        subprocess.check_call(["dnssec-trigger-control", "status"], stdout=DEVNULL, stderr=DEVNULL)
Pavel Šimerda c6b3534
+        with Lock():
Pavel Šimerda c6b3534
+            subprocess.check_call(["dnssec-trigger-control", "status"], stdout=DEVNULL, stderr=DEVNULL)
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+            connections = None
Pavel Šimerda c6b3534
+            if self.config.use_vpn_global_forwarders:
Pavel Šimerda c6b3534
+                connections = list(ConnectionList(self.client, only_vpn=True))
Pavel Šimerda c6b3534
+            if not connections:
Pavel Šimerda c6b3534
+                connections = list(ConnectionList(self.client, only_default=True))
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
-        default_connections = ConnectionList(only_default=True)
Pavel Šimerda c6b3534
-        servers = Store('servers')
Pavel Šimerda c6b3534
+            servers = Store('servers')
Pavel Šimerda c6b3534
 
Pavel Šimerda c6b3534
-        if servers.update(sum((connection.servers for connection in default_connections), [])):
Pavel Šimerda c6b3534
-            subprocess.check_call(["unbound-control", "flush_zone", "."])
Pavel Šimerda c6b3534
-            subprocess.check_call(["dnssec-trigger-control", "submit"] + list(servers))
Pavel Šimerda c6b3534
-            servers.commit()
Pavel Šimerda c6b3534
-        log.info("Global forwarders: {}".format(' '.join(servers)))
Pavel Šimerda c6b3534
+            if servers.update(sum((connection.servers for connection in connections), [])):
Pavel Šimerda c6b3534
+                subprocess.check_call(["unbound-control", "flush_zone", "."])
Pavel Šimerda c6b3534
+                subprocess.check_call(["dnssec-trigger-control", "submit"] + list(servers))
Pavel Šimerda c6b3534
+                servers.commit()
Pavel Šimerda c6b3534
+            log.info("Global forwarders: {}".format(' '.join(servers)))
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
     def run_update_connection_zones(self):
Pavel Šimerda 654266c
         """Configures forward zones in the unbound using unbound-control."""
Pavel Šimerda 654266c
 
Pavel Šimerda 654266c
-        connections = ConnectionList(skip_wifi=not self.config.add_wifi_provided_zones).get_zone_connection_mapping()
Pavel Šimerda c6b3534
-        unbound_zones = UnboundZoneConfig()
Pavel Šimerda c6b3534
-        stored_zones = Store('zones')
Pavel Šimerda c6b3534
-
Pavel Šimerda c6b3534
-        # The purpose of the zone store is to keep the list of Unbound zones
Pavel Šimerda c6b3534
-        # that are managed by dnssec-trigger-script. We don't want to track
Pavel Šimerda c6b3534
-        # zones accoss Unbound restarts. We want to clear any Unbound zones
Pavel Šimerda c6b3534
-        # that are no longer active in NetworkManager.
Pavel Šimerda c6b3534
-        log.debug("removing stored zones not present in both unbound and an active connection")
Pavel Šimerda c6b3534
-        for zone in stored_zones:
Pavel Šimerda c6b3534
-            if zone not in unbound_zones:
Pavel Šimerda c6b3534
-                stored_zones.remove(zone)
Pavel Šimerda c6b3534
-            elif zone not in connections:
Pavel Šimerda c6b3534
-                unbound_zones.remove(zone)
Pavel Šimerda c6b3534
-                stored_zones.remove(zone)
Pavel Šimerda c6b3534
-
Pavel Šimerda c6b3534
-        # We need to install zones that are not yet in Unbound. We also need to
Pavel Šimerda c6b3534
-        # reinstall zones that are already managed by dnssec-trigger in case their
Pavel Šimerda c6b3534
-        # list of nameservers was changed.
Pavel Šimerda c6b3534
-        #
Pavel Šimerda c6b3534
-        # TODO: In some cases, we don't seem to flush Unbound cache properly,
Pavel Šimerda c6b3534
-        # even when Unbound is restarted (and dnssec-trigger as well, because
Pavel Šimerda c6b3534
-        # of dependency).
Pavel Šimerda c6b3534
-        log.debug("installing connection provided zones")
Pavel Šimerda c6b3534
-        for zone in connections:
Pavel Šimerda c6b3534
-            if zone in stored_zones or zone not in unbound_zones:
Pavel Šimerda c6b3534
-                unbound_zones.add(zone, connections[zone].servers, secure=self.config.validate_connection_provided_zones)
Pavel Šimerda c6b3534
-                stored_zones.add(zone)
Pavel Šimerda c6b3534
-
Pavel Šimerda c6b3534
-        stored_zones.commit()
Pavel Šimerda c6b3534
+        with Lock():
Pavel Šimerda c6b3534
+            connections = ConnectionList(self.client, skip_wifi=not self.config.add_wifi_provided_zones).get_zone_connection_mapping()
Pavel Šimerda c6b3534
+            unbound_zones = UnboundZoneConfig()
Pavel Šimerda c6b3534
+            stored_zones = Store('zones')
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+            # The purpose of the zone store is to keep the list of Unbound zones
Pavel Šimerda c6b3534
+            # that are managed by dnssec-trigger-script. We don't want to track
Pavel Šimerda c6b3534
+            # zones accoss Unbound restarts. We want to clear any Unbound zones
Pavel Šimerda c6b3534
+            # that are no longer active in NetworkManager.
Pavel Šimerda c6b3534
+            log.debug("removing stored zones not present in both unbound and an active connection")
Pavel Šimerda c6b3534
+            for zone in stored_zones:
Pavel Šimerda c6b3534
+                if zone not in unbound_zones:
Pavel Šimerda c6b3534
+                    stored_zones.remove(zone)
Pavel Šimerda c6b3534
+                elif zone not in connections:
Pavel Šimerda c6b3534
+                    unbound_zones.remove(zone)
Pavel Šimerda c6b3534
+                    stored_zones.remove(zone)
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+            # We need to install zones that are not yet in Unbound. We also need to
Pavel Šimerda c6b3534
+            # reinstall zones that are already managed by dnssec-trigger in case their
Pavel Šimerda c6b3534
+            # list of nameservers was changed.
Pavel Šimerda c6b3534
+            #
Pavel Šimerda c6b3534
+            # TODO: In some cases, we don't seem to flush Unbound cache properly,
Pavel Šimerda c6b3534
+            # even when Unbound is restarted (and dnssec-trigger as well, because
Pavel Šimerda c6b3534
+            # of dependency).
Pavel Šimerda c6b3534
+            log.debug("installing connection provided zones")
Pavel Šimerda c6b3534
+            for zone in connections:
Pavel Šimerda c6b3534
+                if zone in stored_zones or zone not in unbound_zones:
Pavel Šimerda c6b3534
+                    unbound_zones.add(zone, connections[zone].servers, secure=self.config.validate_connection_provided_zones)
Pavel Šimerda c6b3534
+                    stored_zones.add(zone)
Pavel Šimerda c6b3534
+
Pavel Šimerda c6b3534
+            stored_zones.commit()
Pavel Šimerda 654266c
 
Pavel Šimerda c6b3534
 if __name__ == "__main__":
Pavel Šimerda c6b3534
     try: