def service_connect_to_server(self):
        """
        Attempt to connect to the silent dune server.
        :return:
        """

        self._connected = False
        self._connection_start = False

        self._sds_conn = SilentDuneConnection(self._server, self._no_tls,
                                              self._port)
        _logger.info('{0}: Server -> {1}:{2} use-tls: {3}'.format(
            self.get_name(), self._server, self._port, not self._no_tls))

        if self._sds_conn.connect_with_machine_id(self.node_info.machine_id):
            self._connected = True
            self._connection_start = True
            self._node, status_code = self._sds_conn.get_node_by_machine_id(
                self.node_info.machine_id)

            if self._node:
                self._t_check_interval = (self._node.polling_interval *
                                          60) + self._t_random_seconds
            else:
                _logger.critical(
                    '{0}: !!! node information retrieval failed'.format(
                        self.get_name()))
        else:
            # Check to see if
            _logger.warning(
                '{0}: Failed to connect with server, will attempt reconnection.'
                .format(self.get_name()))

        return self._connected
    def install_module(self):
        """
        Virtual Override
        Register and download our bundle information from the server.
        """

        if not self._enabled:
            return True

        self._sds_conn = SilentDuneConnection(self._server, self._no_tls,
                                              self._port)

        if not self._sds_conn.connect_with_password(self._user,
                                                    self._password):
            return False

        self.get_global_preferences()

        if not self._register_node():
            return False

        if not self._get_rule_bundle():
            return False

        if not self._set_node_bundle():
            return False

        # TODO: Get and Upload adapter interface list to server
        # Note: It might be better to call ifconfig instead of using netifaces to get adapter info.

        # self._write_bundleset_to_file()

        return True
示例#3
0
    def service_connect_to_server(self):
        """
        Attempt to connect to the silent dune server.
        :return:
        """

        self._connected = False
        self._connection_start = False

        self._sds_conn = SilentDuneConnection(self._server, self._no_tls, self._port)
        _logger.info('{0}: Server -> {1}:{2} use-tls: {3}'.format(
            self.get_name(), self._server, self._port, not self._no_tls))

        if self._sds_conn.connect_with_machine_id(self.node_info.machine_id):
            self._connected = True
            self._connection_start = True
            self._node, status_code = self._sds_conn.get_node_by_machine_id(self.node_info.machine_id)

            if self._node:
                self._t_check_interval = (self._node.polling_interval * 60) + self._t_random_seconds
            else:
                _logger.critical('{0}: !!! node information retrieval failed'.format(self.get_name()))
        else:
            # Check to see if
            _logger.warning('{0}: Failed to connect with server, will attempt reconnection.'.format(self.get_name()))

        return self._connected
示例#4
0
    def install_module(self):
        """
        Virtual Override
        Register and download our bundle information from the server.
        """

        if not self._enabled:
            return True

        self._sds_conn = SilentDuneConnection(self._server, self._no_tls, self._port)

        if not self._sds_conn.connect_with_password(self._user, self._password):
            return False

        self.get_global_preferences()

        if not self._register_node():
            return False

        if not self._get_rule_bundle():
            return False

        if not self._set_node_bundle():
            return False

        # TODO: Get and Upload adapter interface list to server
        # Note: It might be better to call ifconfig instead of using netifaces to get adapter info.

        # self._write_bundleset_to_file()

        return True
示例#5
0
class BaseServerPasswordConnection(TestCase):
    """
    Base class that other unit tests can inherit to automatically get a mock server connection.
    """

    sdc = None

    def setUp(self):

        self.make_connection()

    @mock.patch('silentdune_client.modules.comm.sd_server.connection.requests.get', autospec=True,
                side_effect=mock_server_requests_get)
    @mock.patch('silentdune_client.modules.comm.sd_server.connection.requests.post', autospec=True,
                side_effect=mock_server_requests_post)
    def make_connection(self, mock_get, mock_post):

        self.sdc = SilentDuneConnection('127.0.0.1', True, 80)
        self.sdc.connect_with_password('abcdabcd', '12341234')
class BaseServerPasswordConnection(TestCase):
    """
    Base class that other unit tests can inherit to automatically get a mock server connection.
    """

    sdc = None

    def setUp(self):

        self.make_connection()

    @mock.patch(
        'silentdune_client.modules.comm.sd_server.connection.requests.get',
        autospec=True,
        side_effect=mock_server_requests_get)
    @mock.patch(
        'silentdune_client.modules.comm.sd_server.connection.requests.post',
        autospec=True,
        side_effect=mock_server_requests_post)
    def make_connection(self, mock_get, mock_post):

        self.sdc = SilentDuneConnection('127.0.0.1', True, 80)
        self.sdc.connect_with_password('abcdabcd', '12341234')
示例#7
0
class SilentDuneServerModule(BaseModule):
    """ Silent Dune Server Module """

    _enabled = False

    # Module properties
    _server = ''
    _port = 0
    _no_tls = False
    _bundle_name = ''
    _user = ''
    _password = ''

    _flat_rules = None

    # Server Connection
    _sds_conn = None
    _connected = False
    _connection_start = False

    # Server objects
    _node = None
    _bundle = None
    _node_bundle = None
    _bundle_machine_subsets = None
    _globals = None

    # Timed events.
    _t_connection_retry = 0  # Timed event property for retrying connection to server.
    _t_next_check = 0  # Timed event used to calculate next time we should contact the server.

    # Random number of seconds between -30 and 30 to make sure all nodes do not
    # contact the server at the same time.
    _t_random_seconds = int((random() - 0.5) * 60.0)
    _t_check_interval = 0

    priority = 30

    # Status
    _locked = False
    _startup = True

    def __init__(self):

        self._arg_name = 'server'  # Set argparser name
        self._config_section_name = 'server_module'  # Set configuration file section name

        # Enable multi-threading
        self.wants_processing_thread = True

        self._enabled = False

        try:
            self._version = pkg_resources.get_distribution(__name__).version
        except:
            self._version = 'unknown'

    def add_installer_arguments(self, parser):
        """
        Virtual Override
        Add our module's argparser arguments
        """

        # Create a argument group for our module
        group = parser.add_argument_group('server module', 'Silent Dune Server module')

        group.add_argument('--server-mod-enable', action='store_true', help=_('Enable the server module'))  # noqa

        group.add_argument(_('--server'), help=_('Silent Dune server network address (required)'),  # noqa
                        default=None, type=str, metavar='IP')  # noqa
        group.add_argument(_('--server-bundle'), help=_('Firewall bundle to use for this node (required)'),  # noqa
                        default=None, type=str, metavar='BUNDLE')  # noqa
        group.add_argument(_('--server-user'), help=_('Server admin user name (required)'),  # noqa
                        default=None, type=str, metavar='USER')  # noqa
        group.add_argument(_('--server-password'), help=_('Server admin password (required)'),  # noqa
                        default=None, type=str, metavar='PASSWORD')  # noqa
        group.add_argument(_('--server-no-tls'), help=_('Do not use a TLS connection'),  # noqa
                        default=False, action='store_true')  # noqa
        group.add_argument(_('--server-port'), help=_('Use alternate http port'),  # noqa
                        default=0, type=int, metavar='PORT')  # noqa

    def validate_arguments(self, args):
        """
        Virtual Override
        Validate command line arguments and save values to our configuration object.
        :param args: An argparse object.
        """
        # See if we have been enabled or not
        if '--server-mod-enable' not in sys.argv:
            return True

        if args.server_mod_enable:
            self._enabled = True

        if not args.server:
            print('sdc-install: argument --server is required.')
            return False
        if not args.server_bundle:
            print('sdc-install: argument --server-bundle is required.')
            return False
        if not args.server_user:
            print('sdc-install: argument --server-user is required.')
            return False
        if not args.server_password:
            print('sdc-install: argument --server-password is required.')
            return False

        # Check for valid IPv4 address
        if '.' in args.server:
            if not is_valid_ipv4_address(args.server):
                print('sdc-install: argument --server is invalid ip address')
                return False

        # Check for valid IPv6 address
        if ':' in args.server:
            if not is_valid_ipv6_address(args.server):
                print('sdc-install: argument --server is invalid ip address')
                return False

        self._server = args.server
        self._port = args.server_port
        self._no_tls = args.server_no_tls
        self._bundle_name = args.server_bundle

        # User and password are only used during the install process
        self._user = args.server_user
        self._password = args.server_password

        return True

    def validate_config(self, config):
        """
        Virtual Override
        Validate configuration file arguments and save values to our config object.
        :param config: A ConfigParser object.
        """
        # See if we are enabled or not
        try:
            self._enabled = True if config.get(self._config_section_name, 'enabled').lower() == 'yes' else False
        except:
            _logger.debug('{0} configuration section not found in configuration file.'.format(
                self._config_section_name))
            self._enabled = False

        # Only worry about the rest of the configuration items if we are enabled.
        if self._enabled:

            server = config.get(self._config_section_name, 'server')

            # Check for valid IPv4 address
            if '.' in server:
                if not is_valid_ipv4_address(server):
                    _logger.error('{0}: Config value for "server" is invalid ip address'.format(self.get_name()))
                    return False

            # Check for valid IPv6 address
            if ':' in server:
                if not is_valid_ipv6_address(server):
                    _logger.error('{0}: Config value for "server" is invalid ip address'.format(self.get_name()))
                    return False

            self._server = config.get(self._config_section_name, 'server')
            self._no_tls = False if config.get(self._config_section_name, 'use_tls').lower() == 'yes' else True

            # Make sure the port value is an integer and in range
            try:
                self._port = int(config.get(self._config_section_name, 'port'))
                if not (1 < self._port <= 65536):
                    _logger.error('{0}: port value out of range ({1})'.format(self.get_name(), self._port))
            except ValueError:
                self._port = 80 if self._no_tls else 443

            self._bundle_name = config.get(self._config_section_name, 'bundle')

        return True

    def prepare_config(self, config):
        """
        Virtual Override
        Return the configuration file structure. Any new configuration items should be added here.
        Note: The order should be reverse of the expected order in the configuration file.
        """

        config.set(self._config_section_name, 'enabled', 'yes' if self._enabled else 'no')
        config.set(self._config_section_name, 'server', self._server)
        config.set(self._config_section_name, 'port', self._port)
        config.set(self._config_section_name, 'use_tls', 'no' if self._no_tls else 'yes')
        config.set(self._config_section_name, 'bundle', self._bundle_name)

        config.set_comment(self._config_section_name, self._config_section_name,
                           _('; Silent Dune Server Module Configuration\n'))  # noqa
        config.set_comment(self._config_section_name, 'server',
                           _('; The Silent Dune management server to connect with.\n'))  # noqa
        config.set_comment(self._config_section_name, 'port',
                           _('; The port used by the management server. If no port is given this\n'  # noqa
                            '; node will use port 80 or 443 to connect to the management server\n'
                            '; depending on if the --no-tls option was used during the install.\n'))
        config.set_comment(self._config_section_name, 'use_tls',
                           _('; Use a secure connection when communicating with the management server.'))  # noqa
        config.set_comment(self._config_section_name, 'bundle',
                           _('; Name of the Bundle assigned to this node. Changing this value has\n'  # noqa
                             '; no affect. The client always uses the bundle information assigned\n'
                             '; by the server.'))

        return True

    def install_module(self):
        """
        Virtual Override
        Register and download our bundle information from the server.
        """

        if not self._enabled:
            return True

        self._sds_conn = SilentDuneConnection(self._server, self._no_tls, self._port)

        if not self._sds_conn.connect_with_password(self._user, self._password):
            return False

        self.get_global_preferences()

        if not self._register_node():
            return False

        if not self._get_rule_bundle():
            return False

        if not self._set_node_bundle():
            return False

        # TODO: Get and Upload adapter interface list to server
        # Note: It might be better to call ifconfig instead of using netifaces to get adapter info.

        # self._write_bundleset_to_file()

        return True

    def service_connect_to_server(self):
        """
        Attempt to connect to the silent dune server.
        :return:
        """

        self._connected = False
        self._connection_start = False

        self._sds_conn = SilentDuneConnection(self._server, self._no_tls, self._port)
        _logger.info('{0}: Server -> {1}:{2} use-tls: {3}'.format(
            self.get_name(), self._server, self._port, not self._no_tls))

        if self._sds_conn.connect_with_machine_id(self.node_info.machine_id):
            self._connected = True
            self._connection_start = True
            self._node, status_code = self._sds_conn.get_node_by_machine_id(self.node_info.machine_id)

            if self._node:
                self._t_check_interval = (self._node.polling_interval * 60) + self._t_random_seconds
            else:
                _logger.critical('{0}: !!! node information retrieval failed'.format(self.get_name()))
        else:
            # Check to see if
            _logger.warning('{0}: Failed to connect with server, will attempt reconnection.'.format(self.get_name()))

        return self._connected

    def service_startup(self):
        _logger.debug('{0}: module startup called'.format(self.get_name()))

        return True

    def service_shutdown(self):
        _logger.debug('{0}: module shutdown called'.format(self.get_name()))

        # Notify the server we are no longer active.
        if self._connected:
            self.update_server_node_info(active=False)

        return True

    def _add_server_access_rule(self):
        """
        Add a management server connection rule so we can connect
        :return:
        """

        rule = create_iptables_egress_ingress_rule(
            self._server, self._port, 'tcp', Slots.silentdune_server, transport=ipt.TRANSPORT_AUTO)

        # Notify the firewall module to reload the rules.
        task = QueueTask(TASK_FIREWALL_INSERT_RULES,
                         src_module=self.get_name(),
                         dest_module=SilentDuneClientFirewallModule().get_name(),
                         data=rule)
        self.send_parent_task(task)

    def process_loop(self):

        if self._startup:
            self._startup = False

            self._add_server_access_rule()
            time.sleep(3)  # Give the firewall time to add the rules before we attempt to connect

            self.service_connect_to_server()

            if self._sds_conn.status == ConnStatus.not_registered:
                _logger.error('{0} this node is not registered, unable to download rules'.format(self.get_name()))

        # If we are not registered, just return
        if self._sds_conn and self._sds_conn.status == ConnStatus.not_registered:
            return

        # If we are not connected to the server, try reconnecting every 60 seconds.
        if not self._connected:
            if self.timed_event('_t_connection_retry', 60):
                self._t_connection_retry = self.t_seconds
                self.service_connect_to_server()

        if self._connected:

            # Check to see if the connection to the server has just started.
            if self._connection_start:

                # Get the global preferences
                self.get_global_preferences()

                # Notify the server we are active now.
                self._node.active = True
                self.update_server_node_info(active=True)

                # Set our lock down mode value.
                self.set_lockdown_mode()

                # Update our firewall with rules from the server.
                self.update_node_firewall_rules_from_server()

                self._connection_start = False

                # Subscribe to log events
                task = QueueTask(TASK_SUBSCRIBE_LOG_EVENTS,
                                 src_module=self.get_name(),
                                 dest_module=SilentDuneClientLoggingModule().get_name())
                self.send_parent_task(task)

                _logger.debug('Next server check in {0} seconds.'.format(self._t_check_interval))

            # Check to see if the node rule bundle information has changed.
            if self.timed_event('_t_next_check', self._t_check_interval):
                _logger.debug('Next server check in {0} seconds.'.format(self._t_check_interval))

                self._node, status_code = self._sds_conn.get_node_by_machine_id(self.node_info.machine_id)

                # Check to see if we need to update our firewall rules bundle.
                if self._node.sync:
                    _logger.info('{0}: Found signal to update the firewall rules bundle.'.format(self.get_name()))

                    self.set_lockdown_mode()
                    self.update_node_firewall_rules_from_server()

                    # Update our information with the server.
                    self.update_server_node_info(sync=False)

    def process_task(self, task):

        if task:
            t_id = task.get_task_id()

            if t_id == TASK_LOG_EVENT:
                _logger.debug('{0}: received log event.'.format(self.get_name()))
                if self.node_info.platform == 'linux':

                    data = task.get_data()  # IPLogEvent object
                    data.node = self._node.id
                    self._sds_conn.iptables_send_alert(data)

    def set_lockdown_mode(self):
        """
        Set the lock down mode
        :return:
        """
        # Check locked down mode.
        if not self._locked and self._node.locked:
            _logger.warning('System is now in lock down mode.')
            self._locked = True

        if self._locked and not self._node.locked:
            _logger.warning('System is no longer in lock down mode.')
            self._locked = False

    def get_global_preferences(self):

        self._globals, status_code = self._sds_conn.get_global_preferences()

        if not globals or status_code != requests.codes.ok:
            _logger.error('Failed to download global preferences. Err: {0}'.format(status_code))
            self._globals = GlobalPreferences(lockdown_slot_level=120)
            return False

        return True

    def update_server_node_info(self, **kwargs):

        # _logger.debug(self._node.to_json())

        data = {u'machine_id': self._node.machine_id}  # machine_id is always required to update a node data record.

        if 'sync' in kwargs:
            data[u'sync'] = kwargs['sync']
            self._node.sync = kwargs['sync']

        if 'active' in kwargs:
            self._node.active = kwargs['active']
            data[u'active'] = kwargs['active']
        else:
            data[u'active'] = self._node.active

        reply, status_code = self._sds_conn.update_node(self._node.id, data)

        # _logger.debug(self._node.to_json())

        if not self._node or status_code != requests.codes.ok:
            _logger.error('{0}: Failed to update node information.'.format(self.get_name()))
            return False

        return True

    def update_node_firewall_rules_from_server(self):
        """
        Retrieve the node bundle rules from the server and send them to the Firewall module.
        :return:
        """

        # TODO: Save current bundle machine subsets and then look for any orphaned sets and remove them
        # from the firewall.

        if self._bundle_machine_subsets:
            # Until the orphan rule check is in place, just tell the firewall to delete all rules.
            task = QueueTask(TASK_FIREWALL_DELETE_RULES,
                             src_module=self.get_name(),
                             dest_module=SilentDuneClientFirewallModule().get_name(),
                             data=self._bundle_machine_subsets)
            self.send_parent_task(task)

        # Get updated bundle information.
        self._node_bundle, status_code = self._sds_conn.get_node_bundle_by_node_id(self._node.id)
        self._bundle, status_code = self._sds_conn.get_bundle_by_id(self._node_bundle.bundle)

        if not self._download_bundleset():
            if self._sds_conn.request_error:
                _logger.error('{0}: request to server failed, changing state to not connected'.format(self.get_name()))
                self._connected = False
                return False

        if self._bundle_machine_subsets and len(self._bundle_machine_subsets) > 0:

            rules = self.flatten_rules(self._bundle_machine_subsets)

            # Check to see if we are in lockdown mode. If so filter out all
            if self._locked:
                data = list()
                for i in rules:
                    if i.slot <= self._globals.lockdown_slot_level or i.slot >= self._globals.rejection_slot_level:
                        data.append(i)
            else:
                data = rules

            # Notify the firewall module to reload the rules.
            task = QueueTask(TASK_FIREWALL_INSERT_RULES,
                             src_module=self.get_name(),
                             dest_module=SilentDuneClientFirewallModule().get_name(),
                             data=data)
            self.send_parent_task(task)

            # Notify the firewall module to reload the rules.
            # task = QueueTask(TASK_FIREWALL_RELOAD_RULES,
            #                  src_module=self.get_name(),
            #                  dest_module=SilentDuneClientFirewallModule().get_name())
            # self.send_parent_task(task)

            # Reset the node rule bundle check timer
            self._t_next_check = self.t_seconds

        else:
            _logger.error('{0}: No rules downloaded from server, unable to update firewall module.'.format(
                self.get_name()))
            return False

        return True

        # TODO: Port knocker event triggers.  https://github.com/moxie0/knockknock

        # Every 10 seconds, send the firewall module a QueueTask
        # if self._seconds_t > self._event_t and self._seconds_t % 4 == 0.0:
        #     self._event_t = self._seconds_t
        #
        #     _logger.debug('Sending {0} module a task.'.format(type(SilentDuneClientFirewallModule).__name__))
        #
        #     task = QueueTask(TASK_FIREWALL_RELOAD_RULES,
        #                      self.get_name(),
        #                      SilentDuneClientFirewallModule().get_name(),
        #                      None)
        #
        #     self.send_parent_task(task)

    def _register_node(self):
        """
        Contact the server to register this node with the server.
        """
        # Look for existing Node record.
        self._node, status_code = self._sds_conn.get_node_by_machine_id(self.node_info.machine_id)

        if status_code == requests.codes.ok and self._node and self._node.id:
            _logger.warning('{0}: Node already registered, using previously registered node info.'.format(
                self.get_name()))
            return True

        self.cwrite('{0}: Registering Node...  '.format(self.get_name()))

        node = Node(
            platform=self.node_info.firewall_platform,
            os=platform.system().lower(),
            dist=platform.dist()[0],
            dist_version=platform.dist()[1],
            hostname=socket.gethostname(),
            python_version=sys.version.replace('\n', ''),
            machine_id=self.node_info.machine_id,
            fernet_key='',  # Fernet.generate_key().decode('UTF-8'),
            polling_interval=self._globals.polling_interval
        )

        # Attempt to register this node on the SD server.
        self._node, status_code = self._sds_conn.register_node(node)

        if status_code != requests.codes.created or not self._node or self._node.id is None:
            self.cwriteline('[Failed]', 'Register Node failed, unknown reason.')
            return False

        self.cwriteline('[OK]', 'Node successfully registered.')

        return True

    def _get_rule_bundle(self):

        if self._bundle_name:

            self.cwrite('Looking up rule bundle...')

            self._bundle, status_code = self._sds_conn.get_bundle_by_name(self._bundle_name)

            if self._bundle and self._bundle.id > 0:
                self.cwriteline('[OK]', 'Found rule bundle.')
                return True

            self.cwriteline('[Failed]', 'Unable to find rule bundle named "{0}".'.format(self._bundle_name))

            # _logger.warning(_("Unable to find the rule bundle specified. The installer can try to lookup "  # noqa
            #                   "and use the default server rule bundle."))  # noqa
            #
            # self.cwrite(_('Do you want to use the server default rule bundle? [y/N]:'))  # noqa
            # result = sys.stdin.read(1)
            #
            # if result not in {'y', 'Y'}:
            #     _logger.debug('User aborting installation process.')
            #     return False

        self.cwrite('Looking up the server default rule bundle...')

        self._bundle, status_code = self._sds_conn.get_default_bundle()

        if not self._bundle or self._bundle.id is None:
            self.cwriteline('[Failed]', 'Default bundle lookup failed.')
            return False

        self.cwriteline('[OK]', 'Found default rule bundle.')

        return True

    def _set_node_bundle(self):

        self.cwrite('Setting Node rule bundle...')

        data = NodeBundle(node=self._node.id, bundle=self._bundle.id)

        self._node_bundle, status_code = self._sds_conn.create_node_bundle(data)

        if not self._node_bundle:
            self.cwriteline('[Failed]', 'Unable to set Node rule bundle.')
            return False

        self.cwriteline('[OK]', 'Node rule bundle successfully set.')
        return True

    def _download_bundleset(self):

        # Get the machineset IDs assigned to the bundle
        self._bundle_machine_subsets, status_code = self._sds_conn.get_bundle_machine_subsets(self._node_bundle)

        if status_code == requests.codes.ok:
            if self._bundle_machine_subsets is None:
                _logger.warning('{0}: no rules found in bundle'.format(self.get_name()))

            return True

        _logger.error('{0}: failed to download rule bundle from server ({1})'.format(self.get_name(), status_code))
        return False

    def flatten_rules(self, obj, level=0):
        """
        Takes nested rules and recursively un-winds them.
        :param obj: Rules object
        :param level: recursion level counter
        :return: rule list
        """

        if level == 0:
            self._flat_rules = list()

        try:

            # Test obj to see if it is really a rule
            if obj.to_json().encode('utf-8'):

                match = False

                for rule in self._flat_rules:
                    if hashlib.sha1(rule.to_json().encode('utf-8')).hexdigest() == \
                            hashlib.sha1(obj.to_json().encode('utf-8')).hexdigest():
                        match = True

                # Don't add rules that already exist in our flat list.
                if not match:
                    self._flat_rules.append(obj)

        except AttributeError:
            if obj and len(obj) > 0:
                for o in obj:
                    self.flatten_rules(o, (level+1))

        if level == 0:
            rules = self._flat_rules
            self._flat_rules = None  # clear list.
            return rules
class SilentDuneServerModule(BaseModule):
    """ Silent Dune Server Module """

    _enabled = False

    # Module properties
    _server = ''
    _port = 0
    _no_tls = False
    _bundle_name = ''
    _user = ''
    _password = ''

    _flat_rules = None

    # Server Connection
    _sds_conn = None
    _connected = False
    _connection_start = False

    # Server objects
    _node = None
    _bundle = None
    _node_bundle = None
    _bundle_machine_subsets = None
    _globals = None

    # Timed events.
    _t_connection_retry = 0  # Timed event property for retrying connection to server.
    _t_next_check = 0  # Timed event used to calculate next time we should contact the server.

    # Random number of seconds between -30 and 30 to make sure all nodes do not
    # contact the server at the same time.
    _t_random_seconds = int((random() - 0.5) * 60.0)
    _t_check_interval = 0

    priority = 30

    # Status
    _locked = False
    _startup = True

    def __init__(self):

        self._arg_name = 'server'  # Set argparser name
        self._config_section_name = 'server_module'  # Set configuration file section name

        # Enable multi-threading
        self.wants_processing_thread = True

        self._enabled = False

        try:
            self._version = pkg_resources.get_distribution(__name__).version
        except:
            self._version = 'unknown'

    def add_installer_arguments(self, parser):
        """
        Virtual Override
        Add our module's argparser arguments
        """

        # Create a argument group for our module
        group = parser.add_argument_group('server module',
                                          'Silent Dune Server module')

        group.add_argument('--server-mod-enable',
                           action='store_true',
                           help=_('Enable the server module'))  # noqa

        group.add_argument(
            _('--server'),
            help=_('Silent Dune server network address (required)'),  # noqa
            default=None,
            type=str,
            metavar='IP')  # noqa
        group.add_argument(
            _('--server-bundle'),
            help=_('Firewall bundle to use for this node (required)'),  # noqa
            default=None,
            type=str,
            metavar='BUNDLE')  # noqa
        group.add_argument(
            _('--server-user'),
            help=_('Server admin user name (required)'),  # noqa
            default=None,
            type=str,
            metavar='USER')  # noqa
        group.add_argument(
            _('--server-password'),
            help=_('Server admin password (required)'),  # noqa
            default=None,
            type=str,
            metavar='PASSWORD')  # noqa
        group.add_argument(
            _('--server-no-tls'),
            help=_('Do not use a TLS connection'),  # noqa
            default=False,
            action='store_true')  # noqa
        group.add_argument(
            _('--server-port'),
            help=_('Use alternate http port'),  # noqa
            default=0,
            type=int,
            metavar='PORT')  # noqa

    def validate_arguments(self, args):
        """
        Virtual Override
        Validate command line arguments and save values to our configuration object.
        :param args: An argparse object.
        """
        # See if we have been enabled or not
        if '--server-mod-enable' not in sys.argv:
            return True

        if args.server_mod_enable:
            self._enabled = True

        if not args.server:
            print('sdc-install: argument --server is required.')
            return False
        if not args.server_bundle:
            print('sdc-install: argument --server-bundle is required.')
            return False
        if not args.server_user:
            print('sdc-install: argument --server-user is required.')
            return False
        if not args.server_password:
            print('sdc-install: argument --server-password is required.')
            return False

        # Check for valid IPv4 address
        if '.' in args.server:
            if not is_valid_ipv4_address(args.server):
                print('sdc-install: argument --server is invalid ip address')
                return False

        # Check for valid IPv6 address
        if ':' in args.server:
            if not is_valid_ipv6_address(args.server):
                print('sdc-install: argument --server is invalid ip address')
                return False

        self._server = args.server
        self._port = args.server_port
        self._no_tls = args.server_no_tls
        self._bundle_name = args.server_bundle

        # User and password are only used during the install process
        self._user = args.server_user
        self._password = args.server_password

        return True

    def validate_config(self, config):
        """
        Virtual Override
        Validate configuration file arguments and save values to our config object.
        :param config: A ConfigParser object.
        """
        # See if we are enabled or not
        try:
            self._enabled = True if config.get(
                self._config_section_name,
                'enabled').lower() == 'yes' else False
        except:
            _logger.debug(
                '{0} configuration section not found in configuration file.'.
                format(self._config_section_name))
            self._enabled = False

        # Only worry about the rest of the configuration items if we are enabled.
        if self._enabled:

            server = config.get(self._config_section_name, 'server')

            # Check for valid IPv4 address
            if '.' in server:
                if not is_valid_ipv4_address(server):
                    _logger.error(
                        '{0}: Config value for "server" is invalid ip address'.
                        format(self.get_name()))
                    return False

            # Check for valid IPv6 address
            if ':' in server:
                if not is_valid_ipv6_address(server):
                    _logger.error(
                        '{0}: Config value for "server" is invalid ip address'.
                        format(self.get_name()))
                    return False

            self._server = config.get(self._config_section_name, 'server')
            self._no_tls = False if config.get(
                self._config_section_name,
                'use_tls').lower() == 'yes' else True

            # Make sure the port value is an integer and in range
            try:
                self._port = int(config.get(self._config_section_name, 'port'))
                if not (1 < self._port <= 65536):
                    _logger.error('{0}: port value out of range ({1})'.format(
                        self.get_name(), self._port))
            except ValueError:
                self._port = 80 if self._no_tls else 443

            self._bundle_name = config.get(self._config_section_name, 'bundle')

        return True

    def prepare_config(self, config):
        """
        Virtual Override
        Return the configuration file structure. Any new configuration items should be added here.
        Note: The order should be reverse of the expected order in the configuration file.
        """

        config.set(self._config_section_name, 'enabled',
                   'yes' if self._enabled else 'no')
        config.set(self._config_section_name, 'server', self._server)
        config.set(self._config_section_name, 'port', self._port)
        config.set(self._config_section_name, 'use_tls',
                   'no' if self._no_tls else 'yes')
        config.set(self._config_section_name, 'bundle', self._bundle_name)

        config.set_comment(
            self._config_section_name, self._config_section_name,
            _('; Silent Dune Server Module Configuration\n'))  # noqa
        config.set_comment(
            self._config_section_name, 'server',
            _('; The Silent Dune management server to connect with.\n'
              ))  # noqa
        config.set_comment(
            self._config_section_name,
            'port',
            _('; The port used by the management server. If no port is given this\n'  # noqa
              '; node will use port 80 or 443 to connect to the management server\n'
              '; depending on if the --no-tls option was used during the install.\n'
              ))
        config.set_comment(
            self._config_section_name, 'use_tls',
            _('; Use a secure connection when communicating with the management server.'
              ))  # noqa
        config.set_comment(
            self._config_section_name,
            'bundle',
            _('; Name of the Bundle assigned to this node. Changing this value has\n'  # noqa
              '; no affect. The client always uses the bundle information assigned\n'
              '; by the server.'))

        return True

    def install_module(self):
        """
        Virtual Override
        Register and download our bundle information from the server.
        """

        if not self._enabled:
            return True

        self._sds_conn = SilentDuneConnection(self._server, self._no_tls,
                                              self._port)

        if not self._sds_conn.connect_with_password(self._user,
                                                    self._password):
            return False

        self.get_global_preferences()

        if not self._register_node():
            return False

        if not self._get_rule_bundle():
            return False

        if not self._set_node_bundle():
            return False

        # TODO: Get and Upload adapter interface list to server
        # Note: It might be better to call ifconfig instead of using netifaces to get adapter info.

        # self._write_bundleset_to_file()

        return True

    def service_connect_to_server(self):
        """
        Attempt to connect to the silent dune server.
        :return:
        """

        self._connected = False
        self._connection_start = False

        self._sds_conn = SilentDuneConnection(self._server, self._no_tls,
                                              self._port)
        _logger.info('{0}: Server -> {1}:{2} use-tls: {3}'.format(
            self.get_name(), self._server, self._port, not self._no_tls))

        if self._sds_conn.connect_with_machine_id(self.node_info.machine_id):
            self._connected = True
            self._connection_start = True
            self._node, status_code = self._sds_conn.get_node_by_machine_id(
                self.node_info.machine_id)

            if self._node:
                self._t_check_interval = (self._node.polling_interval *
                                          60) + self._t_random_seconds
            else:
                _logger.critical(
                    '{0}: !!! node information retrieval failed'.format(
                        self.get_name()))
        else:
            # Check to see if
            _logger.warning(
                '{0}: Failed to connect with server, will attempt reconnection.'
                .format(self.get_name()))

        return self._connected

    def service_startup(self):
        _logger.debug('{0}: module startup called'.format(self.get_name()))

        return True

    def service_shutdown(self):
        _logger.debug('{0}: module shutdown called'.format(self.get_name()))

        # Notify the server we are no longer active.
        if self._connected:
            self.update_server_node_info(active=False)

        return True

    def _add_server_access_rule(self):
        """
        Add a management server connection rule so we can connect
        :return:
        """

        rule = create_iptables_egress_ingress_rule(
            self._server,
            self._port,
            'tcp',
            Slots.silentdune_server,
            transport=ipt.TRANSPORT_AUTO)

        # Notify the firewall module to reload the rules.
        task = QueueTask(
            TASK_FIREWALL_INSERT_RULES,
            src_module=self.get_name(),
            dest_module=SilentDuneClientFirewallModule().get_name(),
            data=rule)
        self.send_parent_task(task)

    def process_loop(self):

        if self._startup:
            self._startup = False

            self._add_server_access_rule()
            time.sleep(
                3
            )  # Give the firewall time to add the rules before we attempt to connect

            self.service_connect_to_server()

            if self._sds_conn.status == ConnStatus.not_registered:
                _logger.error(
                    '{0} this node is not registered, unable to download rules'
                    .format(self.get_name()))

        # If we are not registered, just return
        if self._sds_conn and self._sds_conn.status == ConnStatus.not_registered:
            return

        # If we are not connected to the server, try reconnecting every 60 seconds.
        if not self._connected:
            if self.timed_event('_t_connection_retry', 60):
                self._t_connection_retry = self.t_seconds
                self.service_connect_to_server()

        if self._connected:

            # Check to see if the connection to the server has just started.
            if self._connection_start:

                # Get the global preferences
                self.get_global_preferences()

                # Notify the server we are active now.
                self._node.active = True
                self.update_server_node_info(active=True)

                # Set our lock down mode value.
                self.set_lockdown_mode()

                # Update our firewall with rules from the server.
                self.update_node_firewall_rules_from_server()

                self._connection_start = False

                # Subscribe to log events
                task = QueueTask(
                    TASK_SUBSCRIBE_LOG_EVENTS,
                    src_module=self.get_name(),
                    dest_module=SilentDuneClientLoggingModule().get_name())
                self.send_parent_task(task)

                _logger.debug('Next server check in {0} seconds.'.format(
                    self._t_check_interval))

            # Check to see if the node rule bundle information has changed.
            if self.timed_event('_t_next_check', self._t_check_interval):
                _logger.debug('Next server check in {0} seconds.'.format(
                    self._t_check_interval))

                self._node, status_code = self._sds_conn.get_node_by_machine_id(
                    self.node_info.machine_id)

                # Check to see if we need to update our firewall rules bundle.
                if self._node.sync:
                    _logger.info(
                        '{0}: Found signal to update the firewall rules bundle.'
                        .format(self.get_name()))

                    self.set_lockdown_mode()
                    self.update_node_firewall_rules_from_server()

                    # Update our information with the server.
                    self.update_server_node_info(sync=False)

    def process_task(self, task):

        if task:
            t_id = task.get_task_id()

            if t_id == TASK_LOG_EVENT:
                _logger.debug('{0}: received log event.'.format(
                    self.get_name()))
                if self.node_info.platform == 'linux':

                    data = task.get_data()  # IPLogEvent object
                    data.node = self._node.id
                    self._sds_conn.iptables_send_alert(data)

    def set_lockdown_mode(self):
        """
        Set the lock down mode
        :return:
        """
        # Check locked down mode.
        if not self._locked and self._node.locked:
            _logger.warning('System is now in lock down mode.')
            self._locked = True

        if self._locked and not self._node.locked:
            _logger.warning('System is no longer in lock down mode.')
            self._locked = False

    def get_global_preferences(self):

        self._globals, status_code = self._sds_conn.get_global_preferences()

        if not globals or status_code != requests.codes.ok:
            _logger.error(
                'Failed to download global preferences. Err: {0}'.format(
                    status_code))
            self._globals = GlobalPreferences(lockdown_slot_level=120)
            return False

        return True

    def update_server_node_info(self, **kwargs):

        # _logger.debug(self._node.to_json())

        data = {
            u'machine_id': self._node.machine_id
        }  # machine_id is always required to update a node data record.

        if 'sync' in kwargs:
            data[u'sync'] = kwargs['sync']
            self._node.sync = kwargs['sync']

        if 'active' in kwargs:
            self._node.active = kwargs['active']
            data[u'active'] = kwargs['active']
        else:
            data[u'active'] = self._node.active

        reply, status_code = self._sds_conn.update_node(self._node.id, data)

        # _logger.debug(self._node.to_json())

        if not self._node or status_code != requests.codes.ok:
            _logger.error('{0}: Failed to update node information.'.format(
                self.get_name()))
            return False

        return True

    def update_node_firewall_rules_from_server(self):
        """
        Retrieve the node bundle rules from the server and send them to the Firewall module.
        :return:
        """

        # TODO: Save current bundle machine subsets and then look for any orphaned sets and remove them
        # from the firewall.

        if self._bundle_machine_subsets:
            # Until the orphan rule check is in place, just tell the firewall to delete all rules.
            task = QueueTask(
                TASK_FIREWALL_DELETE_RULES,
                src_module=self.get_name(),
                dest_module=SilentDuneClientFirewallModule().get_name(),
                data=self._bundle_machine_subsets)
            self.send_parent_task(task)

        # Get updated bundle information.
        self._node_bundle, status_code = self._sds_conn.get_node_bundle_by_node_id(
            self._node.id)
        self._bundle, status_code = self._sds_conn.get_bundle_by_id(
            self._node_bundle.bundle)

        if not self._download_bundleset():
            if self._sds_conn.request_error:
                _logger.error(
                    '{0}: request to server failed, changing state to not connected'
                    .format(self.get_name()))
                self._connected = False
                return False

        if self._bundle_machine_subsets and len(
                self._bundle_machine_subsets) > 0:

            rules = self.flatten_rules(self._bundle_machine_subsets)

            # Check to see if we are in lockdown mode. If so filter out all
            if self._locked:
                data = list()
                for i in rules:
                    if i.slot <= self._globals.lockdown_slot_level or i.slot >= self._globals.rejection_slot_level:
                        data.append(i)
            else:
                data = rules

            # Notify the firewall module to reload the rules.
            task = QueueTask(
                TASK_FIREWALL_INSERT_RULES,
                src_module=self.get_name(),
                dest_module=SilentDuneClientFirewallModule().get_name(),
                data=data)
            self.send_parent_task(task)

            # Notify the firewall module to reload the rules.
            # task = QueueTask(TASK_FIREWALL_RELOAD_RULES,
            #                  src_module=self.get_name(),
            #                  dest_module=SilentDuneClientFirewallModule().get_name())
            # self.send_parent_task(task)

            # Reset the node rule bundle check timer
            self._t_next_check = self.t_seconds

        else:
            _logger.error(
                '{0}: No rules downloaded from server, unable to update firewall module.'
                .format(self.get_name()))
            return False

        return True

        # TODO: Port knocker event triggers.  https://github.com/moxie0/knockknock

        # Every 10 seconds, send the firewall module a QueueTask
        # if self._seconds_t > self._event_t and self._seconds_t % 4 == 0.0:
        #     self._event_t = self._seconds_t
        #
        #     _logger.debug('Sending {0} module a task.'.format(type(SilentDuneClientFirewallModule).__name__))
        #
        #     task = QueueTask(TASK_FIREWALL_RELOAD_RULES,
        #                      self.get_name(),
        #                      SilentDuneClientFirewallModule().get_name(),
        #                      None)
        #
        #     self.send_parent_task(task)

    def _register_node(self):
        """
        Contact the server to register this node with the server.
        """
        # Look for existing Node record.
        self._node, status_code = self._sds_conn.get_node_by_machine_id(
            self.node_info.machine_id)

        if status_code == requests.codes.ok and self._node and self._node.id:
            _logger.warning(
                '{0}: Node already registered, using previously registered node info.'
                .format(self.get_name()))
            return True

        self.cwrite('{0}: Registering Node...  '.format(self.get_name()))

        node = Node(
            platform=self.node_info.firewall_platform,
            os=platform.system().lower(),
            dist=platform.dist()[0],
            dist_version=platform.dist()[1],
            hostname=socket.gethostname(),
            python_version=sys.version.replace('\n', ''),
            machine_id=self.node_info.machine_id,
            fernet_key='',  # Fernet.generate_key().decode('UTF-8'),
            polling_interval=self._globals.polling_interval)

        # Attempt to register this node on the SD server.
        self._node, status_code = self._sds_conn.register_node(node)

        if status_code != requests.codes.created or not self._node or self._node.id is None:
            self.cwriteline('[Failed]',
                            'Register Node failed, unknown reason.')
            return False

        self.cwriteline('[OK]', 'Node successfully registered.')

        return True

    def _get_rule_bundle(self):

        if self._bundle_name:

            self.cwrite('Looking up rule bundle...')

            self._bundle, status_code = self._sds_conn.get_bundle_by_name(
                self._bundle_name)

            if self._bundle and self._bundle.id > 0:
                self.cwriteline('[OK]', 'Found rule bundle.')
                return True

            self.cwriteline(
                '[Failed]', 'Unable to find rule bundle named "{0}".'.format(
                    self._bundle_name))

            # _logger.warning(_("Unable to find the rule bundle specified. The installer can try to lookup "  # noqa
            #                   "and use the default server rule bundle."))  # noqa
            #
            # self.cwrite(_('Do you want to use the server default rule bundle? [y/N]:'))  # noqa
            # result = sys.stdin.read(1)
            #
            # if result not in {'y', 'Y'}:
            #     _logger.debug('User aborting installation process.')
            #     return False

        self.cwrite('Looking up the server default rule bundle...')

        self._bundle, status_code = self._sds_conn.get_default_bundle()

        if not self._bundle or self._bundle.id is None:
            self.cwriteline('[Failed]', 'Default bundle lookup failed.')
            return False

        self.cwriteline('[OK]', 'Found default rule bundle.')

        return True

    def _set_node_bundle(self):

        self.cwrite('Setting Node rule bundle...')

        data = NodeBundle(node=self._node.id, bundle=self._bundle.id)

        self._node_bundle, status_code = self._sds_conn.create_node_bundle(
            data)

        if not self._node_bundle:
            self.cwriteline('[Failed]', 'Unable to set Node rule bundle.')
            return False

        self.cwriteline('[OK]', 'Node rule bundle successfully set.')
        return True

    def _download_bundleset(self):

        # Get the machineset IDs assigned to the bundle
        self._bundle_machine_subsets, status_code = self._sds_conn.get_bundle_machine_subsets(
            self._node_bundle)

        if status_code == requests.codes.ok:
            if self._bundle_machine_subsets is None:
                _logger.warning('{0}: no rules found in bundle'.format(
                    self.get_name()))

            return True

        _logger.error(
            '{0}: failed to download rule bundle from server ({1})'.format(
                self.get_name(), status_code))
        return False

    def flatten_rules(self, obj, level=0):
        """
        Takes nested rules and recursively un-winds them.
        :param obj: Rules object
        :param level: recursion level counter
        :return: rule list
        """

        if level == 0:
            self._flat_rules = list()

        try:

            # Test obj to see if it is really a rule
            if obj.to_json().encode('utf-8'):

                match = False

                for rule in self._flat_rules:
                    if hashlib.sha1(rule.to_json().encode('utf-8')).hexdigest() == \
                            hashlib.sha1(obj.to_json().encode('utf-8')).hexdigest():
                        match = True

                # Don't add rules that already exist in our flat list.
                if not match:
                    self._flat_rules.append(obj)

        except AttributeError:
            if obj and len(obj) > 0:
                for o in obj:
                    self.flatten_rules(o, (level + 1))

        if level == 0:
            rules = self._flat_rules
            self._flat_rules = None  # clear list.
            return rules
    def make_connection(self, mock_get, mock_post):

        self.sdc = SilentDuneConnection('127.0.0.1', True, 80)
        self.sdc.connect_with_password('abcdabcd', '12341234')
示例#10
0
    def make_connection(self, mock_get, mock_post):

        self.sdc = SilentDuneConnection('127.0.0.1', True, 80)
        self.sdc.connect_with_password('abcdabcd', '12341234')