0xV3NOMx
Linux ip-172-26-7-228 5.4.0-1103-aws #111~18.04.1-Ubuntu SMP Tue May 23 20:04:10 UTC 2023 x86_64



Your IP : 18.191.171.136


Current Path : /usr/bin/
Upload File :
Current File : //usr/bin/networkd-dispatcher

#! /usr/bin/python3
# networkd-dispatcher
#   Dispatcher service for systemd-networkd
# Copyright(c) 2016 by wave++ "Yuri D'Elia" <wavexx@thregr.org>
# Distributed under GPLv3+ (see COPYING) WITHOUT ANY WARRANTY.
# Copyright(c) 2018 by craftyguy "Clayton Craft" <clayton@craftyguy.net>
# Distributed under GPLv3+ (see COPYING) WITHOUT ANY WARRANTY.


from __future__ import print_function, division, generators, unicode_literals

import argparse
import collections
import errno
import json
import logging
import os
import pathlib
import socket
import subprocess
import sys

from gi.repository import GLib as glib

import dbus
import dbus.mainloop.glib

logger = logging.getLogger('networkd-dispatcher')


# Detect up-front which commands we use exist
def resolve_path(cmdname):
    for dirname in os.environ['PATH'].split(':'):
        path = os.path.join(dirname, cmdname)
        if os.path.exists(path):
            return path
    logger.warning('No valid path found for %s', cmdname)
    return None


# Constants
NETWORKCTL = resolve_path('networkctl')
DEFAULT_SCRIPT_DIR = '/etc/networkd-dispatcher:/usr/lib/networkd-dispatcher'

# Supported wireless tools
IWCONFIG = resolve_path('iwconfig')
IW = resolve_path('iw')

LOG_FORMAT = '%(levelname)s:%(message)s'

STATE_IGN = {'carrier', 'degraded', None}
SINGLETONS = {'Type', 'ESSID', 'OperationalState'}

# taken from https://www.freedesktop.org/software/systemd/man/networkctl.html
ADMIN_STATES = ['configured', 'configuring', 'failed', 'pending', 'unmanaged',
                'linger', 'initialized']
OPER_STATES = ['carrier', 'degraded', 'degraded-carrier', 'dormant',
               'enslaved', 'missing', 'no-carrier', 'off', 'routable']

AddressList = collections.namedtuple('AddressList', ['ipv4', 'ipv6'])
NetworkctlListState = collections.namedtuple('NetworkctlListState',
                                             ['idx', 'name', 'type',
                                              'operational', 'administrative'])


class UnknownState(Exception):
    pass


def unquote(buf, char='\\'):
    """Remove escape characters from iwconfig ESSID output"""
    idx = 0
    while True:
        idx = buf.find(char, idx)
        if idx < 0:
            break
        buf = buf[:idx] + buf[idx+1:]
        idx += 1
    return buf


def get_networkctl_list():
    """Update the mapping from interface index numbers to state"""
    try:
        out = subprocess.check_output([NETWORKCTL, 'list', '--no-pager',
                                      '--no-legend'])
    except subprocess.CalledProcessError as e:
        logger.error('networkctl list failed: %s', e)
        return []

    result = []
    for line in out.split(b'\n')[:-1]:
        fields = line.decode('utf-8', errors='replace').split()
        idx_s = fields.pop(0)
        result.append(NetworkctlListState(int(idx_s), *fields))
    return result


def get_networkctl_status(iface_name):
    """Return a dictionary mapping keys to lists (or strings if
    in SINGLETONS)"""
    data = collections.defaultdict(list)
    try:
        out = subprocess.check_output([NETWORKCTL, 'status', '--no-pager',
                                      '--no-legend', '--', iface_name])
    except subprocess.CalledProcessError as e:
        logger.error('Failed to get interface "%s" status: %s', iface_name, e)
        return data

    oldk = None
    for line in out.split(b'\n')[1:-1]:
        line = line.decode('utf-8', errors='replace')
        k = line[:16].strip() or oldk
        oldk = k
        v = line[18:].strip()
        if k in SINGLETONS:
            data[k] = v
        else:
            data[k].append(v)
    return data


def get_wlan_essid(iface_name):
    """Given an interface name, return its ESSID"""
    if IWCONFIG is None:
        if IW is None:
            logger.error('Unable to retrieve ESSID for wireless interface %s: '
                         'no supported wireless tool installed' % iface_name)
            return ''
        return iw_get_ssid(iface_name)
    return iwconfig_get_ssid(iface_name)


def iw_get_ssid(iface_name):
    out = subprocess.check_output([IW, iface_name, 'link'])
    lines = out.decode('utf-8', errors='replace').split('\n')
    line = [s for s in lines if 'SSID' in s]
    if len(line) == 0:
        logger.warning('Unable to retrieve ESSID for wireless interface %s.'
                       % iface_name)
        return ''
    essid = line[0].rsplit(" ")[1]
    return unquote(essid)


def iwconfig_get_ssid(iface_name):
    out = subprocess.check_output([IWCONFIG, '--', iface_name])
    line = out.split(b'\n')[0].decode('utf-8', errors='replace')
    essid = line[line.find('ESSID:')+7:-3]
    return unquote(essid)


def check_perms(path, mode=0o755, uid=0, gid=0):
    """ Check that the given file or dir @ path has the given mode set, and is
    owned by the given uid/gid. Symlinks are *not* followed. Raises
    FileNotFoundError if path doesn't exist."""

    if not os.path.exists(path):
        raise FileNotFoundError
    st = os.stat(path, follow_symlinks=False)
    st_mode = st.st_mode & 0x00FFF
    if st.st_uid == uid and st.st_gid == gid and st_mode == mode:
        return True

    logger.error("invalid permissions on %s. expected mode=%s, uid=%d, "
                 "gid=%d; got mode=%s, uid=%d, gid=%d", path, oct(mode), uid,
                 gid, oct(st_mode), st.st_uid, st.st_gid)
    return False


def scripts_in_path(path, subdir):
    """Given directory names in PATH notation (separated by :), and a
    subdirectory name, return a sorted list of executables
    contained in that subdirectory, such that executables in earlier
    path components override those with the same name in later path
    components."""
    script_list = []
    base_filenames = set()
    for one_path in path.split(":"):
        one_path = os.path.join(one_path, subdir)
        if not os.path.exists(one_path):
            logger.debug("Path %r does not exist; skipping", one_path)
            continue
        base_filenames.update(os.listdir(one_path))

    for filename in sorted(base_filenames):
        for one_path in path.split(":"):
            pathname = os.path.join(one_path, subdir, filename)

            if os.path.isfile(pathname):
                try:
                    realpath = pathlib.Path(pathname).resolve()

                    # Make sure that the file's parent dir has the correct
                    # perms, without following any symlinks
                    if not check_perms(os.path.dirname(pathname),
                                       0o755, 0, 0):
                        continue

                    # Make sure file has correct perms, after following any
                    # symlink(s)
                    if not check_perms(realpath, 0o755, 0, 0):
                        continue
                except FileNotFoundError:
                    continue

                script_list.append(pathname)
                break

    return script_list


def parse_address_strings(addrs):
    """Given a list of addresses, discard uninteresting ones, and sort the rest
    into IPv4 vs IPv6"""
    ip4addrs = []
    ip6addrs = []
    for addr in addrs:
        if addr.startswith('127.') or \
           addr.startswith('fe80:'):
            continue
        if ':' in addr:
            ip6addrs.append(addr)
        elif '.' in addr:
            ip4addrs.append(addr)
    return AddressList(ip4addrs, ip6addrs)


def get_interface_data(iface, state):
    """Return JSON-serializable data representing all state needed to run
    hooks for the given interface"""
    data = {'Type': iface.type, 'OperationalState': iface.operational,
            'AdministrativeState': iface.administrative}
    # Always collect what data we can.
    data.update(get_networkctl_status(iface.name))
    # The returned state may be different than what was read from
    # 'networkctl list', so construct state based on th iface data.
    # See Issue #24.
    data['State'] = (data.get('OperationalState', '') + " (" +
                     data.get('AdministrativeState', '') + ")")
    if data.get('Type') == 'wlan':
        data['ESSID'] = get_wlan_essid(iface.name)
    return data


class Dispatcher(object):
    def __init__(self, script_dir=DEFAULT_SCRIPT_DIR):
        self.iface_names_by_idx = {}    # only changed on rescan
        self.ifaces_by_name = {}        # updated on every state change
        self.script_dir = script_dir
        self._interface_scan()

    def __repr__(self):
        return '<Dispatcher(%r)>' % (self.__dict__,)

    def _interface_scan(self):
        iface_list = get_networkctl_list()
        # Append new interfaces, keeping old ones around to avoid hotplug race
        # condition (issue #20)
        for i in iface_list:
            if i not in self.iface_names_by_idx:
                self.iface_names_by_idx[i.idx] = i.name
                self.ifaces_by_name[i.name] = i
        logger.debug('Performed interface scan; state: %r', self)

    def register(self, bus=None):
        """Register this dispatcher to handle events from the given bus"""
        if bus is None:
            bus = dbus.SystemBus()
        bus.add_signal_receiver(self._receive_signal,
                                bus_name='org.freedesktop.network1',
                                signal_name='PropertiesChanged',
                                path_keyword='path')

    def trigger_all(self):
        """Immediately invoke all scripts for the last known (or initial)
        states for each interface"""
        logger.info('Triggering scripts for last-known state for all'
                    'interfaces')
        for iface_name, iface in self.ifaces_by_name.items():
            logger.debug('Running immediate triggers for %r', iface)
            try:
                self.handle_state(iface_name,
                                  administrative_state=iface.administrative,
                                  operational_state=iface.operational,
                                  force=True)
            except UnknownState as e:
                logger.exception("Unknown state for interface %s: %s",
                                 iface, str(e))
            except Exception:  # pylint: disable=broad-except
                logger.exception("Error handling initial state for "
                                 "interface %r", iface)

    def get_scripts_list(self, state):
        """Return scripts for the given state"""
        return scripts_in_path(self.script_dir, state + ".d")

    def _handle_one_state(self, iface_name, state, state_type, force=False):
        """Process a single state change"""
        try:
            if state is None:
                return

            prior_iface = self.ifaces_by_name.get(iface_name)
            if prior_iface is None:
                logger.error('Attempting to handle state for unknown interface'
                             '%r', iface_name)
                return

            prior_state = getattr(prior_iface, state_type)
            if force is False and state == prior_state:
                logger.debug('No change represented by %s state %r for '
                             'interface %r', state_type, state, iface_name)
                return

            new_iface = prior_iface._replace(**{state_type: state})
            self.ifaces_by_name[new_iface.name] = new_iface

            if state in STATE_IGN:
                logger.debug('Ignored state %r seen for interface %r, '
                             'skipping', state, iface_name)
                return

            self.run_hooks_for_state(new_iface, state)
        # pylint: disable=broad-except
        except Exception:
            logger.exception('Error handling notification for interface %r '
                             'entering %s state %s', iface_name, state_type,
                             state)

    def handle_state(self, iface_name, administrative_state=None,
                     operational_state=None, force=False):
        if (administrative_state and
                administrative_state.lower() not in ADMIN_STATES):
            raise UnknownState(administrative_state)
        if (operational_state and
                operational_state.lower() not in OPER_STATES):
            raise UnknownState(operational_state)

        self._handle_one_state(iface_name, administrative_state,
                               'administrative', force=force)
        self._handle_one_state(iface_name, operational_state, 'operational',
                               force=force)

    def run_hooks_for_state(self, iface, state):
        """Run all hooks associated with a given state"""
        # No actions to take? Do nothing.
        script_list = self.get_scripts_list(state)
        if not script_list:
            logger.debug('Ignoring notification for interface %r entering '
                         'state %r: no triggers', iface, state)
            return

        # Collect data
        data = get_interface_data(iface, state)
        (v4addrs, v6addrs) = parse_address_strings(data.get('Address', ()))

        # Set script env. variables
        script_env = dict(os.environ)
        script_env.update({
            'ADDR': (data.get('Address', ['']) + [''])[0],
            'ESSID': data.get('ESSID', ''),
            'IP_ADDRS': ' '.join(v4addrs),
            'IP6_ADDRS': ' '.join(v6addrs),
            'IFACE': iface.name,
            'STATE': str(state),
            'AdministrativeState': data.get('AdministrativeState', ''),
            'OperationalState': data.get('OperationalState', ''),
            'json': json.dumps(data),
        })

        # run all valid scripts in the list
        logger.debug('Running triggers for interface %r entering state %r '
                     'with environment %r', iface, state, script_env)
        for script in script_list:
            logger.info('Invoking %r for interface %s', script, iface.name)
            ret = subprocess.Popen(script, env=script_env).wait()
            if ret != 0:
                logger.warning('Exit status %r from script %r invoked with '
                               'environment %r', ret, script, script_env)

    def _receive_signal(self, typ, data, _, path):
        logger.debug('Signal: typ=%r, data=%r, path=%r', typ, data, path)
        if typ != 'org.freedesktop.network1.Link':
            logger.debug('Ignoring signal received with unexpected typ %r',
                         typ)
            return
        if not path.startswith('/org/freedesktop/network1/link/_'):
            logger.warning('Ignoring signal received with unexpected path %r',
                           path)
            return

        # Detect necessity of reloading map *before* filtering ignored states
        # http://thread.gmane.org/gmane.comp.sysutils.systemd.devel/36460
        idx = path[32:]
        idx = int(chr(int(idx[:2], 16)) + idx[2:])
        if idx not in self.iface_names_by_idx:
            # Try to reload configuration if even an ignored message is seen
            logger.warning('Unknown index %r seen, reloading interface list',
                           idx)
            self._interface_scan()

        try:
            iface_name = self.iface_names_by_idx[idx]
        except KeyError:
            # Presumptive race condition: We reloaded, but the index is
            # still invalid
            logger.error('Unknown interface index %r seen even after reload',
                         idx)
            return

        operational_state = data.get('OperationalState', None)
        administrative_state = data.get('AdministrativeState', None)

        if ((operational_state is not None) or
                (administrative_state is not None)):
            try:
                self.handle_state(iface_name,
                                  administrative_state=str(administrative_state)  # noqa
                                  if administrative_state else None,
                                  operational_state=str(operational_state)
                                  if operational_state else None,)
            except UnknownState as e:
                logger.exception("Unknown state for interface %s: %s",
                                 iface_name, str(e))

        # Handle interfaces that have been removed
        if administrative_state == 'linger':
            try:
                self.iface_names_by_idx.pop(idx)
                self.ifaces_by_name.pop(iface_name)
            except KeyError:
                logger.error('Unable to remove interface at index %r.', idx)
            finally:
                return


def sd_notify(unset_environment=False, **kwargs):
    """Systemd sd_notify implementation for Python.
    Note: kwargs should contain the state to send to systemd"""
    if not kwargs:
        logger.error("sd_notify called with no state specified!")
        return -errno.EINVAL
    sock = None
    try:
        # Turn state, a dictionary, into a properly formatted string where
        # each 'key=val' combo in the dictionary is separated by a \n
        state_str = '\n'.join(['{0}={1}'.format(key, val) for (key, val)
                               in kwargs.items()])
        env = os.environ.get('NOTIFY_SOCKET', None)
        if not env:
            # Process was not invoked with systemd
            return -errno.EINVAL
        if env[0] not in ('/', '@'):
            logger.warning("NOTIFY_SOCKET is set, but does not contain a "
                           "legitimate value")
            return -errno.EINVAL
        if env[0] == '@':
            env = '\0' + env[1:]
        sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
        if sock.sendto(bytearray(state_str, 'utf-8'), env) > 0:
            return 1
    # pylint: disable=broad-except
    except Exception:
        logger.exception("Ignoring unexpected error during sd_notify()"
                         "invocation")

    if sock:
        sock.close()
    if unset_environment:
        if 'NOTIFY_SOCKET' in os.environ:
            del os.environ['NOTIFY_SOCKET']

    return 0


def main():
    ap = argparse.ArgumentParser(description='networkd dispatcher daemon')
    ap.add_argument('-S', '--script-dir', action='store',
                    default=DEFAULT_SCRIPT_DIR,
                    help='Location under which to look for scripts [default: '
                    '%(default)s]')
    ap.add_argument('-T', '--run-startup-triggers', action='store_true',
                    help='Generate events reflecting preexisting state and '
                    'behavior on startup [default: %(default)s]')
    ap.add_argument('-v', '--verbose', action='count', default=0,
                    help='Increment verbosity level once per call')
    ap.add_argument('-q', '--quiet', action='count', default=0,
                    help='Decrement verbosity level once per call')
    args = ap.parse_args()

    verbosity_num = (args.verbose - args.quiet)
    if verbosity_num <= -2:
        log_level = logging.CRITICAL
    elif verbosity_num <= -1:
        log_level = logging.ERROR
    elif verbosity_num == 0:
        log_level = logging.WARNING
    elif verbosity_num == 1:
        log_level = logging.INFO
    else:
        log_level = logging.DEBUG
    logging.basicConfig(level=log_level, format=LOG_FORMAT)

    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

    if NETWORKCTL is None:
        logger.critical('Unable to find networkctl command; cannot continue')
        sd_notify(ERRNO=errno.ENOENT)
        sys.exit(1)

    dispatcher = Dispatcher(script_dir=args.script_dir)
    dispatcher.register()

    # After configuring the receiver, run initial operations
    if args.run_startup_triggers:
        dispatcher.trigger_all()

    # main loop
    mainloop = glib.MainLoop()
    # Signal to systemd that service is runnning
    sd_notify(READY=1)
    logger.info('Startup complete')
    mainloop.run()


if __name__ == '__main__':
    main()

# vim: ai et sts=4 sw=4 ts=4