Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fail2ban'd IPv6 addresses are not processed #4144

Closed
ghost opened this issue Sep 10, 2018 · 15 comments · Fixed by #4168
Closed

Fail2ban'd IPv6 addresses are not processed #4144

ghost opened this issue Sep 10, 2018 · 15 comments · Fixed by #4168
Assignees

Comments

@ghost
Copy link

ghost commented Sep 10, 2018

The fail2ban plugin seems to be ignoring IPv6 bans.

fail2ban-client status sshd returns the following:

`- Actions
   |- Currently banned: 2
   |- Total banned:     2
   `- Banned IP list:   2a0c:2500:953:1d7::1 185.230.161.209

The associated netdata graphs only registered a ban once I got the second (IPv4) address banned.

(only semi-related, please tell me if I should open another issue: the bans/s chart isn't really useful, could you consider replacing it with (or simply adding) a currently banned chart?)

Thanks!

@Ferroin
Copy link
Member

Ferroin commented Sep 10, 2018

I think the module pre-dates proper IPv6 support in fail2ban, which would explain why it isn't working correctly here. I'll take a look at it and see if I can get it working correctly, but it may be later this week before I get a chance to do so.

@Ferroin
Copy link
Member

Ferroin commented Sep 10, 2018

After a quick look, I can confirm that it is indeed a case of the module only looking for IPv4 addresses. I'll see if I can update the regex that's being used to match the log lines so it can pull out IPv6 addresses too. Hopefully things will just work with that change, because I'm not confident that I can figure out exactly how the rest of the plugin is deciding if something is actually banned or not.

@ilyam8
Copy link
Member

ilyam8 commented Sep 11, 2018

Yo @komic
Regex
https://github.com/firehol/netdata/blob/249055928195bc5a6c3916df6a9640e26b0814cc/python.d/fail2ban.chart.py#L19

To add ipv6 support please change

(?P<ipaddr>\d{1,3}(?:\.\d{1,3}){3})

to

(?P<ipaddr>[0-9.a-f]+)

Does it work?

@ilyam8
Copy link
Member

ilyam8 commented Sep 11, 2018

a currently banned chart?

Seem like same chart as Banned IPs (since the last restart of netdata). (except since the last restart of netdata part 😄 )

@ghost
Copy link
Author

ghost commented Sep 11, 2018

@l2isbad it uh, sort of works?

1: fail2ban-client set sshd banip 2a0c:2500:953:1d8::2
2: fail2ban-client set sshd banip 2a0c:2500:953:1d9::2
3: fail2ban-client set sshd banip 2afc:2500:953:1d9::2
4: fail2ban-client set sshd banip 2afc:2500:953:2d9::2

screenshot

all four registered as a new ban on the first chart, but only 1 and 3 registered as a new IP on the second one.

also yeah, they're similar, but knowing how many IPs are currently in a jail is a better metric than how many IPs are added every second to that jail imo

@ilyam8
Copy link
Member

ilyam8 commented Sep 11, 2018

Ok, give me a minute!

@ilyam8
Copy link
Member

ilyam8 commented Sep 11, 2018

@komic

check this (replace current)

fail2ban module

# -*- coding: utf-8 -*-
# Description: fail2ban log netdata python.d module
# Author: l2isbad
# SPDX-License-Identifier: GPL-3.0+

from glob import glob
from re import compile as r_compile
from os import access as is_accessible, R_OK
from os.path import isdir, getsize

from collections import defaultdict

from bases.FrameworkServices.LogService import LogService

priority = 60000
retries = 60
REGEX_JAILS = r_compile(r'\[([a-zA-Z0-9_-]+)\][^\[\]]+?enabled\s+= (true|false)')
REGEX_DATA = r_compile(r'\[(?P<jail>[A-Za-z-_0-9]+)\] (?P<action>Unban|Ban) (?P<ipaddr>[0-9.:a-f]+)')
ORDER = ['jails_bans', 'jails_in_jail']


class Service(LogService):
    """
    fail2ban log class
    Reads logs line by line
    Jail auto detection included
    It produces following charts:
    * Bans per second for every jail
    * Banned IPs for every jail (since the last restart of netdata)
    """
    def __init__(self, configuration=None, name=None):
        LogService.__init__(self, configuration=configuration, name=name)
        self.order = ORDER
        self.definitions = dict()
        self.log_path = self.configuration.get('log_path', '/var/log/fail2ban.log')
        self.conf_path = self.configuration.get('conf_path', '/etc/fail2ban/jail.local')
        self.conf_dir = self.configuration.get('conf_dir', '/etc/fail2ban/jail.d/')
        self.exclude = self.configuration.get('exclude')
        self.in_jail = defaultdict(set)

    def _get_data(self):
        """
        Parse new log lines
        :return: dict
        """
        raw = self._get_raw_data()
        if raw is None:
            return None
        elif not raw:
            return self.to_netdata

        # Fail2ban logs looks like
        # 2016-12-25 12:36:04,711 fail2ban.actions[2455]: WARNING [ssh] Ban 178.156.32.231
        for row in raw:
            match = REGEX_DATA.search(row)

            if not match:
                continue

            match_dict = match.groupdict()
            jail, action, ipaddr = match_dict['jail'], match_dict['action'], match_dict['ipaddr']

            if jail not in self.jails_list:
                continue

            if action == 'Ban':
                self.to_netdata[jail] += 1
                if ipaddr not in self.in_jail[jail]:
                    self.in_jail[jail].add(ipaddr)
                    self.to_netdata[jail + '_in_jail'] += 1
            else:
                if ipaddr in self.in_jail[jail]:
                    self.in_jail[jail].remove(ipaddr)
                    self.to_netdata[jail + '_in_jail'] -= 1

        return self.to_netdata

    def check(self):
        """
        :return: bool
        Check if the "log_path" is not empty and readable
        """

        if not (is_accessible(self.log_path, R_OK) and getsize(self.log_path) != 0):
            self.error('%s is not readable or empty' % self.log_path)
            return False
        self.jails_list, self.to_netdata = self.jails_auto_detection_()
        self.definitions = create_definitions_(self.jails_list)
        self.info('Jails: %s' % self.jails_list)
        return True

    def jails_auto_detection_(self):
        """
        return: <tuple>
        * jails_list - list of enabled jails (['ssh', 'apache', ...])
        * to_netdata - dict ({'ssh': 0, 'ssh_in_jail': 0, ...})
        * banned_ips - here will be stored all the banned ips ({'ssh': ['1.2.3.4', '5.6.7.8', ...], ...})
        """
        raw_jails_list = list()
        jails_list = list()

        for raw_jail in parse_configuration_files_(self.conf_path, self.conf_dir, self.error):
            raw_jails_list.extend(raw_jail)

        for jail, status in raw_jails_list:
            if status == 'true' and jail not in jails_list:
                jails_list.append(jail)
            elif status == 'false' and jail in jails_list:
                jails_list.remove(jail)
        # If for some reason parse failed we still can START with default jails_list.
        jails_list = list(set(jails_list) - set(self.exclude.split()
                                                if isinstance(self.exclude, str) else list())) or ['ssh']

        to_netdata = dict([(jail, 0) for jail in jails_list])
        to_netdata.update(dict([(jail + '_in_jail', 0) for jail in jails_list]))

        return jails_list, to_netdata


def create_definitions_(jails_list):
    """
    Chart definitions creating
    """

    definitions = {
        'jails_bans': {'options': [None, 'Jails Ban Statistics', 'bans/s', 'bans', 'jail.bans', 'line'],
                       'lines': []},
        'jails_in_jail': {'options': [None, 'Banned IPs (since the last restart of netdata)', 'IPs',
                                      'in jail', 'jail.in_jail', 'line'],
                          'lines': []}}
    for jail in jails_list:
        definitions['jails_bans']['lines'].append([jail, jail, 'incremental'])
        definitions['jails_in_jail']['lines'].append([jail + '_in_jail', jail, 'absolute'])

    return definitions


def parse_configuration_files_(jails_conf_path, jails_conf_dir, print_error):
    """
    :param jails_conf_path: <str>
    :param jails_conf_dir: <str>
    :param print_error: <function>
    :return: <tuple>
    Uses "find_jails_in_files" function to find all jails in the "jails_conf_dir" directory
    and in the "jails_conf_path"
    All files must endswith ".local" or ".conf"
    Return order is important.
    According man jail.conf it should be
    * jail.conf
    * jail.d/*.conf (in alphabetical order)
    * jail.local
    * jail.d/*.local (in alphabetical order)
    """
    path_conf, path_local, dir_conf, dir_local = list(), list(), list(), list()

    # Parse files in the directory
    if not (isinstance(jails_conf_dir, str) and isdir(jails_conf_dir)):
        print_error('%s is not a directory' % jails_conf_dir)
    else:
        dir_conf = list(filter(lambda conf: is_accessible(conf, R_OK), glob(jails_conf_dir + '/*.conf')))
        dir_local = list(filter(lambda local: is_accessible(local, R_OK), glob(jails_conf_dir + '/*.local')))
        if not (dir_conf or dir_local):
            print_error('%s is empty or not readable' % jails_conf_dir)
        else:
            dir_conf, dir_local = (find_jails_in_files(dir_conf, print_error),
                                   find_jails_in_files(dir_local, print_error))

    # Parse .conf and .local files
    if isinstance(jails_conf_path, str) and jails_conf_path.endswith(('.local', '.conf')):
        path_conf, path_local = (find_jails_in_files([jails_conf_path.split('.')[0] + '.conf'], print_error),
                                 find_jails_in_files([jails_conf_path.split('.')[0] + '.local'], print_error))

    return path_conf, dir_conf, path_local, dir_local


def find_jails_in_files(list_of_files, print_error):
    """
    :param list_of_files: <list>
    :param print_error: <function>
    :return: <list>
    Open a file and parse it to find all (enabled and disabled) jails
    The output is a list of tuples:
    [('ssh', 'true'), ('apache', 'false'), ...]
    """
    jails_list = list()
    for conf in list_of_files:
        if is_accessible(conf, R_OK):
            with open(conf, 'rt') as f:
                raw_data = f.readlines()
            data = ' '.join(line for line in raw_data if line.startswith(('[', 'enabled')))
            jails_list.extend(REGEX_JAILS.findall(data))
        else:
            print_error('%s is not readable or not exist' % conf)
    return jails_list

@ilyam8
Copy link
Member

ilyam8 commented Sep 11, 2018

but knowing how many IPs are currently in a jail

Banned IPs, second chart.

@ghost
Copy link
Author

ghost commented Sep 11, 2018

yup; that seems to work, thanks a lot!

Banned IPs, second chart.

it doesn't reflect the current state of the jail though, just how many addresses went in (and at some point out of) it since netdata's last restart. Processing the log's Found entries might be interesting, too.

@ktsaou ktsaou added the bug label Sep 11, 2018
@ktsaou
Copy link
Member

ktsaou commented Sep 11, 2018

So, this is a bug. Right?

@ilyam8
Copy link
Member

ilyam8 commented Sep 11, 2018

but knowing how many IPs are currently in a jail
it doesn't reflect the current state of the jail though

Actually it reflects. Banned IPs shows how many IPs currently in a specific jail (since the last restart of netdata).

Processing the log's Found entries might be interesting

What for do we need this?

So, this is a bug. Right?

No. Currently this is enhancement - IPv6 addresses support.

@ktsaou ktsaou added enhancement and removed bug labels Sep 11, 2018
@ktsaou
Copy link
Member

ktsaou commented Sep 11, 2018

ok

@ilyam8
Copy link
Member

ilyam8 commented Sep 11, 2018

it doesn't reflect the current state of the jail though, just how many addresses went in (and at some point out of) it since netdata's last restart.

The problem you talking about is since the last restart of netdata thing. Module parses log file (tails) so there is no way to get current state of the jail at start (even read the whole log from head is not valid option imo).

Execute fail2ban-client status JAIL_NAME for every jail at start seems not an option too.

@ghost
Copy link
Author

ghost commented Sep 11, 2018

Sorry for the confusion, it does process Unban entries, I'm guessing it just looked a bit weird because I restarted netdata a few times during my tests. This looks okay to close once #4144 (comment) is merged. (:

@ktsaou
Copy link
Member

ktsaou commented Sep 12, 2018

merged it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants