Blame SOURCES/01-dnssec-trigger-hook

0c84e1
#!/usr/bin/python2
0c84e1
# -*- coding: utf-8 -*-
0c84e1
"""
0c84e1
@author: Tomas Hozza <thozza@redhat.com>
0c84e1
"""
0c84e1
0c84e1
from gi.repository import NMClient
0c84e1
import socket
0c84e1
import struct
0c84e1
import subprocess
0c84e1
import os
0c84e1
import os.path
0c84e1
import syslog
0c84e1
import sys
0c84e1
0c84e1
0c84e1
# DO NOT CHANGE THE VALUE HERE, CHANGE IT IN **DNSSEC_CONF** file
0c84e1
DEFAULT_VALIDATE_FORWARD_ZONES = True
0c84e1
DEFAULT_ADD_WIFI_PROVIDED_ZONES = False
0c84e1
0c84e1
STATE_DIR = "/var/run/dnssec-trigger"
0c84e1
DNSSEC_CONF = "/etc/dnssec.conf"
0c84e1
0c84e1
UNBOUND = "/usr/sbin/unbound"
0c84e1
UNBOUND_CONTROL = "/usr/sbin/unbound-control"
0c84e1
DNSSEC_TRIGGER = "/usr/sbin/dnssec-triggerd"
0c84e1
DNSSEC_TRIGGER_CONTROL = "/usr/sbin/dnssec-trigger-control"
0c84e1
PIDOF = "/usr/sbin/pidof"
0c84e1
0c84e1
0c84e1
class FZonesConfig:
0c84e1
0c84e1
    """
0c84e1
    Class representing dnssec-trigger script forward zones behaviour
0c84e1
    configuration.
0c84e1
    """
0c84e1
0c84e1
    def __init__(self):
0c84e1
        self.validate_fzones = DEFAULT_VALIDATE_FORWARD_ZONES
0c84e1
        self.add_wifi_zones = DEFAULT_ADD_WIFI_PROVIDED_ZONES
0c84e1
0c84e1
0c84e1
class ActiveConnection:
0c84e1
0c84e1
    """
0c84e1
    Simple class representing NM Active Connection with information relevant
0c84e1
    for this script.
0c84e1
    """
0c84e1
0c84e1
    TYPE_WIFI = "WIFI"
0c84e1
    TYPE_VPN = "VPN"
0c84e1
    TYPE_OTHER = "OTHER"
0c84e1
0c84e1
    def __init__(self):
0c84e1
        self.type = self.TYPE_OTHER
0c84e1
        self.is_default = False
0c84e1
        self.nameservers = []
0c84e1
        self.domains = []
0c84e1
        self.uuid = ""
0c84e1
        pass
0c84e1
0c84e1
    def __str__(self):
0c84e1
        string = "UUID: " + self.get_uuid() + "\n"
0c84e1
        string += "TYPE: " + str(self.get_type()) + "\n"
0c84e1
        string += "DEFAULT: " + str(self.get_is_default()) + "\n"
0c84e1
        string += "NS: " + str(self.get_nameservers()) + "\n"
0c84e1
        string += "DOMAINS: " + str(self.get_domains())
0c84e1
        return string
0c84e1
0c84e1
    def get_uuid(self):
0c84e1
        return self.uuid
0c84e1
0c84e1
    def get_type(self):
0c84e1
        return self.type
0c84e1
0c84e1
    def get_is_default(self):
0c84e1
        return self.is_default
0c84e1
0c84e1
    def get_nameservers(self):
0c84e1
        return self.nameservers
0c84e1
0c84e1
    def get_domains(self):
0c84e1
        return self.domains
0c84e1
0c84e1
    def set_uuid(self, uuid=""):
0c84e1
        self.uuid = uuid
0c84e1
0c84e1
    def set_type(self, conn_type=TYPE_OTHER):
0c84e1
        if conn_type == self.TYPE_VPN:
0c84e1
            self.type = self.TYPE_VPN
0c84e1
        elif conn_type == self.TYPE_WIFI:
0c84e1
            self.type = self.TYPE_WIFI
0c84e1
        else:
0c84e1
            self.type = self.TYPE_OTHER
0c84e1
0c84e1
    def set_is_default(self, is_default=True):
0c84e1
        self.is_default = is_default
0c84e1
0c84e1
    def set_nameservers(self, servers=[]):
0c84e1
        self.nameservers = servers
0c84e1
0c84e1
    def set_domains(self, domains=[]):
0c84e1
        self.domains = domains
0c84e1
0c84e1
0c84e1
def ip4_to_str(ip4):
0c84e1
    """
0c84e1
    Converts IPv4 address from integer to string.
0c84e1
    """
0c84e1
    return socket.inet_ntop(socket.AF_INET, struct.pack("=I", ip4))
0c84e1
0c84e1
0c84e1
def ip6_to_str(ip6):
0c84e1
    """
0c84e1
    Converts IPv6 address from integer to string.
0c84e1
    """
0c84e1
    addr_struct = ip6
0c84e1
    return socket.inet_ntop(socket.AF_INET6, addr_struct)
0c84e1
0c84e1
0c84e1
def get_fzones_settings_from_conf(conf_file=""):
0c84e1
    """
0c84e1
    Reads the forward zones behaviour config from file.
0c84e1
    """
0c84e1
    config = FZonesConfig()
0c84e1
0c84e1
    try:
0c84e1
        with open(conf_file, "r") as f:
0c84e1
            lines = [l.strip()
0c84e1
                     for l in f.readlines() if l.strip() and not l.strip().startswith("#")]
0c84e1
            for line in lines:
0c84e1
                option_line = line.split("=")
0c84e1
                if option_line:
0c84e1
                    if option_line[0].strip() == "validate_connection_provided_zones":
0c84e1
                        if option_line[1].strip() == "yes":
0c84e1
                            config.validate_fzones = True
0c84e1
                        else:
0c84e1
                            config.validate_fzones = False
0c84e1
                    elif option_line[0].strip() == "add_wifi_provided_zones":
0c84e1
                        if option_line[1].strip() == "yes":
0c84e1
                            config.add_wifi_zones = True
0c84e1
                        else:
0c84e1
                            config.add_wifi_zones = False
0c84e1
    except IOError:
0c84e1
        # we don't mind if the config file does not exist
0c84e1
        pass
0c84e1
0c84e1
    return config
0c84e1
0c84e1
0c84e1
def get_nm_active_connections():
0c84e1
    """
0c84e1
    Process Active Connections from NM and return list of ActiveConnection
0c84e1
    objects. Active Connections from NM without nameservers are ignored.
0c84e1
    """
0c84e1
    result = []
0c84e1
    client = NMClient.Client()
0c84e1
    ac = client.get_active_connections()
0c84e1
0c84e1
    for connection in ac:
0c84e1
        new_connection = ActiveConnection()
0c84e1
0c84e1
        # get the UUID
0c84e1
        new_connection.set_uuid(connection.get_uuid())
0c84e1
0c84e1
        # Find out if the ActiveConnection is VPN, WIFI or OTHER
0c84e1
        try:
0c84e1
            connection.get_vpn_state()
0c84e1
        except AttributeError:
0c84e1
            # We don't need to change anything
0c84e1
            pass
0c84e1
        else:
0c84e1
            new_connection.set_type(ActiveConnection.TYPE_VPN)
0c84e1
0c84e1
        # if the connection is NOT VPN, then check if it's WIFI
0c84e1
        if new_connection.get_type() != ActiveConnection.TYPE_VPN:
0c84e1
            try:
0c84e1
                device_type = connection.get_devices()[
0c84e1
                    0].get_device_type().value_name
0c84e1
            except IndexError:
0c84e1
                # if there is no device for a connection, the connection
0c84e1
                # is going down so ignore it...
0c84e1
                continue
0c84e1
            except AttributeError:
0c84e1
                # We don't need to change anything
0c84e1
                pass
0c84e1
            else:
0c84e1
                if device_type == "NM_DEVICE_TYPE_WIFI":
0c84e1
                    new_connection.set_type(ActiveConnection.TYPE_WIFI)
0c84e1
0c84e1
        # Finc out if default connection for IP4 or IP6
0c84e1
        if connection.get_default() or connection.get_default6():
0c84e1
            new_connection.set_is_default(True)
0c84e1
        else:
0c84e1
            new_connection.set_is_default(False)
0c84e1
0c84e1
        # Get nameservers (IP4 + IP6)
0c84e1
        ips = []
0c84e1
        try:
0c84e1
            ips4_int = connection.get_ip4_config().get_nameservers()
0c84e1
        except AttributeError:
0c84e1
            # we don't mind if there are no IP4 nameservers
0c84e1
            pass
0c84e1
        else:
0c84e1
            for ip4 in ips4_int:
0c84e1
                ips.append(ip4_to_str(ip4))
0c84e1
        try:
0c84e1
            num = connection.get_ip6_config().get_num_nameservers()
0c84e1
            for i in range(0,num):
0c84e1
                ips.append(ip6_to_str(connection.get_ip6_config().get_nameserver(i)))
0c84e1
        except AttributeError:
0c84e1
            # we don't mind if there are no IP6 nameservers
0c84e1
            pass
0c84e1
        new_connection.set_nameservers(ips)
0c84e1
0c84e1
        # Get domains (IP4 + IP6)
0c84e1
        domains = []
0c84e1
        try:
0c84e1
            domains.extend(connection.get_ip4_config().get_domains())
0c84e1
        except AttributeError:
0c84e1
            # we don't mind if there are no IP6 domains
0c84e1
            pass
0c84e1
        try:
0c84e1
            domains.extend(connection.get_ip6_config().get_domains())
0c84e1
        except AttributeError:
0c84e1
            # we don't mind if there are no IP6 domains
0c84e1
            pass
0c84e1
        new_connection.set_domains(domains)
0c84e1
0c84e1
        # If there are no nameservers in the connection, it is useless
0c84e1
        if new_connection.get_nameservers():
0c84e1
            result.append(new_connection)
0c84e1
0c84e1
    return result
0c84e1
0c84e1
0c84e1
def is_running(binary=""):
0c84e1
    """
0c84e1
    Checks if the given binary is running.
0c84e1
    """
0c84e1
    if binary:
0c84e1
        sp = subprocess.Popen(PIDOF + " " + binary,
0c84e1
                              stdout=subprocess.PIPE,
0c84e1
                              stderr=open(os.devnull, "wb"),
0c84e1
                              shell=True)
0c84e1
        sp.wait()
0c84e1
        if sp.returncode == 0:
0c84e1
            # pidof returns "0" if at least one program with the name runs
0c84e1
            return True
0c84e1
    return False
0c84e1
0c84e1
0c84e1
def dnssec_trigger_set_global_ns(servers=[]):
0c84e1
    """
0c84e1
    Configures global nameservers into dnssec-trigger.
0c84e1
    """
0c84e1
    if servers:
0c84e1
        servers_list = " ".join(servers)
0c84e1
        ret = subprocess.call(
0c84e1
            DNSSEC_TRIGGER_CONTROL + " submit " + servers_list,
0c84e1
            stdout=open(os.devnull, "wb"),
0c84e1
            stderr=subprocess.STDOUT,
0c84e1
            shell=True)
0c84e1
        if ret == 0:
0c84e1
            syslog.syslog(
0c84e1
                syslog.LOG_INFO, "Global forwarders added: " + servers_list)
0c84e1
        else:
0c84e1
            syslog.syslog(
0c84e1
                syslog.LOG_ERR, "Global forwarders NOT added: " + servers_list)
0c84e1
0c84e1
0c84e1
def unbound_add_forward_zone(domain="", servers=[], secure=DEFAULT_VALIDATE_FORWARD_ZONES):
0c84e1
    """
0c84e1
    Adds a forward zone into the unbound.
0c84e1
    """
0c84e1
    if domain and servers:
0c84e1
        servers_list = " ".join(servers)
0c84e1
        # build the command
0c84e1
        cmd = UNBOUND_CONTROL + " forward_add"
0c84e1
        if not secure:
0c84e1
            cmd += " +i"
0c84e1
        cmd += " " + domain + " " + servers_list
0c84e1
        # Add the forward zone
0c84e1
        ret = subprocess.call(cmd,
0c84e1
                              stdout=open(os.devnull, "wb"),
0c84e1
                              stderr=subprocess.STDOUT,
0c84e1
                              shell=True)
0c84e1
        # Flush cache
0c84e1
        subprocess.call(UNBOUND_CONTROL + " flush_zone " + domain,
0c84e1
                        stdout=open(os.devnull, "wb"),
0c84e1
                        stderr=subprocess.STDOUT,
0c84e1
                        shell=True)
0c84e1
        subprocess.call(UNBOUND_CONTROL + " flush_requestlist",
0c84e1
                        stdout=open(os.devnull, "wb"),
0c84e1
                        stderr=subprocess.STDOUT,
0c84e1
                        shell=True)
0c84e1
0c84e1
        if secure:
0c84e1
            validated = "(DNSSEC validated)"
0c84e1
        else:
0c84e1
            validated = "(*NOT* DNSSEC validated)"
0c84e1
0c84e1
        if ret == 0:
0c84e1
            syslog.syslog(
0c84e1
                syslog.LOG_INFO, "Added " + validated + " connection provided forward zone '" + domain + "' with NS: " + servers_list)
0c84e1
        else:
0c84e1
            syslog.syslog(
0c84e1
                syslog.LOG_ERR, "NOT added connection provided forward zone '" + domain + "' with NS: " + servers_list)
0c84e1
0c84e1
0c84e1
def unbound_del_forward_zone(domain="", secure=DEFAULT_VALIDATE_FORWARD_ZONES):
0c84e1
    """
0c84e1
    Deletes a forward zone from the unbound.
0c84e1
    """
0c84e1
    if domain:
0c84e1
        cmd = UNBOUND_CONTROL + " forward_remove"
0c84e1
        if not secure:
0c84e1
            cmd += " +i"
0c84e1
        cmd += " " + domain
0c84e1
        # Remove the forward zone
0c84e1
        ret = subprocess.call(cmd,
0c84e1
                              stdout=open(os.devnull, "wb"),
0c84e1
                              stderr=subprocess.STDOUT,
0c84e1
                              shell=True)
0c84e1
        # Flush cache
0c84e1
        subprocess.call(UNBOUND_CONTROL + " flush_zone " + domain,
0c84e1
                        stdout=open(os.devnull, "wb"),
0c84e1
                        stderr=subprocess.STDOUT,
0c84e1
                        shell=True)
0c84e1
        subprocess.call(UNBOUND_CONTROL + " flush_requestlist",
0c84e1
                        stdout=open(os.devnull, "wb"),
0c84e1
                        stderr=subprocess.STDOUT,
0c84e1
                        shell=True)
0c84e1
        if ret == 0:
0c84e1
            syslog.syslog(
0c84e1
                syslog.LOG_INFO, "Removed connection provided forward zone '" + domain + "'")
0c84e1
        else:
0c84e1
            syslog.syslog(
0c84e1
                syslog.LOG_ERR, "NOT removed connection provided forward zone '" + domain + "'")
0c84e1
0c84e1
0c84e1
def unbound_get_forward_zones():
0c84e1
    """
0c84e1
    Returns list of currently configured forward zones from the unbound.
0c84e1
    """
0c84e1
    zones = []
0c84e1
    # get all configured forward zones
0c84e1
    sp = subprocess.Popen(UNBOUND_CONTROL + " list_forwards",
0c84e1
                          stdout=subprocess.PIPE,
0c84e1
                          stderr=open(os.devnull, "wb"),
0c84e1
                          shell=True)
0c84e1
0c84e1
    sp.wait()
0c84e1
0c84e1
    if sp.returncode == 0:
0c84e1
        for line in sp.stdout.readlines():
0c84e1
            zones.append(line.strip().split(" ")[0][:-1])
0c84e1
0c84e1
    return zones
0c84e1
0c84e1
##############################################################################
0c84e1
0c84e1
0c84e1
def append_fzone_to_file(uuid="", zone=""):
0c84e1
    """
0c84e1
    Append forward zones from connection with UUID to the disk file.
0c84e1
    """
0c84e1
    if uuid and zone:
0c84e1
        with open(os.path.join(STATE_DIR, uuid), "a") as f:
0c84e1
            f.write(zone + "\n")
0c84e1
0c84e1
0c84e1
def write_fzones_to_file(uuid="", zones=[]):
0c84e1
    """
0c84e1
    Write forward zones from connection with UUID to the disk file.
0c84e1
    """
0c84e1
    if uuid and zones:
0c84e1
        with open(os.path.join(STATE_DIR, uuid), "w") as f:
0c84e1
            for zone in zones:
0c84e1
                f.write(zone + "\n")
0c84e1
0c84e1
0c84e1
def get_fzones_from_file(uuid=""):
0c84e1
    """
0c84e1
    Gets all zones from a file with specified UUID name din STATE_DIR
0c84e1
    """
0c84e1
    zones = []
0c84e1
    if uuid:
0c84e1
        with open(os.path.join(STATE_DIR, uuid), "r") as f:
0c84e1
            zones = [line.strip() for line in f.readlines()]
0c84e1
    return zones
0c84e1
0c84e1
0c84e1
def get_fzones_from_disk():
0c84e1
    """
0c84e1
    Gets all forward zones from the disk STATE_DIR.
0c84e1
    Return a dict of "zone" : "connection UUID"
0c84e1
    """
0c84e1
    zones = {}
0c84e1
    conn_files = os.listdir(STATE_DIR)
0c84e1
    for uuid in conn_files:
0c84e1
        for zone in get_fzones_from_file(uuid):
0c84e1
            zones[zone] = uuid
0c84e1
    return zones
0c84e1
0c84e1
0c84e1
def del_all_fzones_from_file(uuid="", secure=DEFAULT_VALIDATE_FORWARD_ZONES):
0c84e1
    """
0c84e1
    Removes all forward zones contained in file with UUID name in STATE_DIR.
0c84e1
    """
0c84e1
    if uuid:
0c84e1
        with open(os.path.join(STATE_DIR, uuid), "r") as f:
0c84e1
            for line in f.readlines():
0c84e1
                unbound_del_forward_zone(line.strip(), secure)
0c84e1
0c84e1
0c84e1
def del_fzones_for_nonexisting_conn(ac=[], secure=DEFAULT_VALIDATE_FORWARD_ZONES):
0c84e1
    """
0c84e1
    Removes all forward zones contained in file (in STATE_DIR) for non-existing
0c84e1
    active connections.
0c84e1
    """
0c84e1
    ac_uuid_list = [conn.get_uuid() for conn in ac]
0c84e1
    conn_files = os.listdir(STATE_DIR)
0c84e1
    # Remove all non-existing connections zones
0c84e1
    for uuid in conn_files:
0c84e1
        if uuid not in ac_uuid_list:
0c84e1
            # remove all zones from the file
0c84e1
            del_all_fzones_from_file(uuid, secure)
0c84e1
            # remove the file
0c84e1
            os.unlink(os.path.join(STATE_DIR, uuid))
0c84e1
0c84e1
0c84e1
def del_fzone_from_file(uuid="", zone=""):
0c84e1
    """
0c84e1
    Deletes a zone from file and writes changes into it. If there are no zones
0c84e1
    left, the file is deleted.
0c84e1
    """
0c84e1
    if uuid and zone:
0c84e1
        zones = get_fzones_from_file(uuid)
0c84e1
        zones.remove(zone)
0c84e1
        if zones:
0c84e1
            write_fzones_to_file(uuid, zones)
0c84e1
        else:
0c84e1
            os.unlink(os.path.join(STATE_DIR, uuid))
0c84e1
0c84e1
0c84e1
##############################################################################
0c84e1
0c84e1
0c84e1
def configure_global_forwarders(active_connections=[]):
0c84e1
    """
0c84e1
    Configure global forwarders using dnssec-trigger-control
0c84e1
    """
0c84e1
    # get only default connections
0c84e1
    default_conns = filter(lambda x: x.get_is_default(), active_connections)
0c84e1
    # get forwarders from default connections
0c84e1
    default_forwarders = []
0c84e1
    for conn in default_conns:
0c84e1
        default_forwarders.extend(conn.get_nameservers())
0c84e1
0c84e1
    if default_forwarders:
0c84e1
        dnssec_trigger_set_global_ns(default_forwarders)
0c84e1
0c84e1
##############################################################################
0c84e1
0c84e1
0c84e1
def configure_forward_zones(active_connections=[], fzones_config=None):
0c84e1
    """
0c84e1
    Configures forward zones in the unbound using unbound-control.
0c84e1
    """
0c84e1
    # Filter out WIFI connections if desirable
0c84e1
    if not fzones_config.add_wifi_zones:
0c84e1
        connections = filter(
0c84e1
            lambda x: x.get_type() != ActiveConnection.TYPE_WIFI, active_connections)
0c84e1
    else:
0c84e1
        connections = active_connections
0c84e1
    # If validate forward zones
0c84e1
    secure = fzones_config.validate_fzones
0c84e1
0c84e1
    # Filter active connections with domain(s)
0c84e1
    conns_with_domains = filter(lambda x: x.get_domains(), connections)
0c84e1
    fzones_from_ac = {}
0c84e1
    # Construct dict of domain -> active connection
0c84e1
    for conn in conns_with_domains:
0c84e1
        # iterate through all domains in the active connection
0c84e1
        for domain in conn.get_domains():
0c84e1
            # if there is already such a domain
0c84e1
            if domain in fzones_from_ac:
0c84e1
                # if the "conn" is VPN and the conn for existing domain is not
0c84e1
                if fzones_from_ac[domain].get_type() != ActiveConnection.TYPE_VPN and conn.get_type() == ActiveConnection.TYPE_VPN:
0c84e1
                    fzones_from_ac[domain] = conn
0c84e1
                # if none of there connections are VPNs or both are VPNs,
0c84e1
                # prefer the default one
0c84e1
                elif not fzones_from_ac[domain].get_is_default() and conn.get_is_default():
0c84e1
                    fzones_from_ac[domain] = conn
0c84e1
            else:
0c84e1
                fzones_from_ac[domain] = conn
0c84e1
0c84e1
    # Remove all zones which connection UUID does not match any existing AC
0c84e1
    del_fzones_for_nonexisting_conn(conns_with_domains, secure)
0c84e1
0c84e1
    # Remove all zones which connection UUID is different than the current AC
0c84e1
    # UUID for the zone
0c84e1
    fzones_from_disk = get_fzones_from_disk()
0c84e1
    for zone, uuid in fzones_from_disk.iteritems():
0c84e1
        connection = fzones_from_ac[zone]
0c84e1
        # if the AC UUID is NOT the same as from the disk, remove the zone
0c84e1
        if connection.get_uuid() != uuid:
0c84e1
            unbound_del_forward_zone(zone, secure)
0c84e1
            del_fzone_from_file(uuid, zone)
0c84e1
0c84e1
    # get zones from unbound and delete them from fzones_from_ac
0c84e1
    # there may be zones manually configured in unbound.conf and we
0c84e1
    # don't want to replace them
0c84e1
    unbound_zones = unbound_get_forward_zones()
0c84e1
    for zone in unbound_zones:
0c84e1
        try:
0c84e1
            del fzones_from_ac[zone]
0c84e1
        except KeyError:
0c84e1
            # we don't mind if there is no such zone
0c84e1
            pass
0c84e1
0c84e1
    # Add forward zones that are not already configured
0c84e1
    fzones_from_disk = get_fzones_from_disk()
0c84e1
    for zone, connection in fzones_from_ac.iteritems():
0c84e1
        if zone not in fzones_from_disk:
0c84e1
            unbound_add_forward_zone(
0c84e1
                zone, connection.get_nameservers(), secure)
0c84e1
            append_fzone_to_file(connection.get_uuid(), zone)
0c84e1
0c84e1
0c84e1
##############################################################################
0c84e1
0c84e1
0c84e1
if __name__ == "__main__":
0c84e1
    if not is_running(DNSSEC_TRIGGER):
0c84e1
        syslog.syslog(syslog.LOG_ERR, "dnssec-triggerd daemon is not running!")
0c84e1
        sys.exit(1)
0c84e1
    if not is_running(UNBOUND):
0c84e1
        syslog.syslog(syslog.LOG_ERR, "unbound server daemon is not running!")
0c84e1
        sys.exit(1)
0c84e1
0c84e1
    fzones_config = get_fzones_settings_from_conf(DNSSEC_CONF)
0c84e1
0c84e1
    # Get all actove connections from NM
0c84e1
    ac = get_nm_active_connections()
0c84e1
    # Configure global forwarders
0c84e1
    configure_global_forwarders(ac)
0c84e1
    # Configure forward zones
0c84e1
    configure_forward_zones(ac, fzones_config)