Blame SOURCES/01-dnssec-trigger-hook

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