Passed
Push — develop ( b816f7...36a25b )
by Nikolay
05:47
created

Fail2BanConf::fail2banIsRunning()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 12
rs 10
cc 3
nc 2
nop 0
1
<?php
2
/*
3
 * Copyright (C) MIKO LLC - All Rights Reserved
4
 * Unauthorized copying of this file, via any medium is strictly prohibited
5
 * Proprietary and confidential
6
 * Written by Nikolay Beketov, 8 2020
7
 *
8
 */
9
10
namespace MikoPBX\Core\System\Configs;
11
12
use MikoPBX\Common\Models\Fail2BanRules;
13
use MikoPBX\Common\Models\NetworkFilters;
14
use MikoPBX\Core\System\MikoPBXConfig;
15
use MikoPBX\Core\System\System;
16
use MikoPBX\Core\System\Util;
17
use MikoPBX\Core\System\Verify;
18
use Phalcon\Di\Injectable;
19
use Phalcon\Text;
20
use SQLite3;
21
22
class Fail2BanConf extends Injectable
23
{
24
    public const FILTER_PATH = '/etc/fail2ban/filter.d';
25
26
    public const FAIL2BAN_DB_PATH = '/var/lib/fail2ban/fail2ban.sqlite3';
27
28
    public bool $fail2ban_enable;
29
30
    /**
31
     * Fail2Ban constructor.
32
     */
33
    public function __construct()
34
    {
35
        $mikoPBXConfig         = new MikoPBXConfig();
36
        $fail2ban_enable       = $mikoPBXConfig->getGeneralSettings('PBXFail2BanEnabled');
37
        $this->fail2ban_enable = ($fail2ban_enable === '1');
38
    }
39
40
    /**
41
     * Check fail2ban service and restart it died
42
     */
43
    public static function checkFail2ban(): void
44
    {
45
        $fail2ban = new self();
46
        if ($fail2ban->fail2ban_enable
47
            && ! $fail2ban->fail2banIsRunning()) {
48
            $fail2ban->fail2banStart();
49
        }
50
    }
51
52
    /**
53
     * Check fail2ban service status
54
     *
55
     * @return bool
56
     */
57
    private function fail2banIsRunning(): bool
58
    {
59
        $fail2banPath = Util::which('fail2ban-client');
60
        $res_ping     = Util::mwExec("{$fail2banPath} ping");
61
        $res_stat     = Util::mwExec("{$fail2banPath} status");
62
63
        $result = false;
64
        if ($res_ping === 0 && $res_stat === 0) {
65
            $result = true;
66
        }
67
68
        return $result;
69
    }
70
71
    /**
72
     * Start fail2ban service
73
     */
74
    public function fail2banStart(): void
75
    {
76
        if (Util::isSystemctl()) {
77
            $systemctlPath = Util::which('systemctl');
78
            Util::mwExec("{$systemctlPath} restart fail2ban");
79
80
            return;
81
        }
82
        // Чистим битые строки, не улдаленные после отмены бана.
83
        $this->cleanFail2banDb();
84
        Util::killByName('fail2ban-server');
85
        $fail2banPath = Util::which('fail2ban-client');
86
        $cmd_start    = "{$fail2banPath} -x start";
87
        $command      = "($cmd_start;) > /dev/null 2>&1 &";
88
        Util::mwExec($command);
89
    }
90
91
    /**
92
     * Cleans all fail2ban blocks
93
     */
94
    public function cleanFail2banDb(): void
95
    {
96
        /** @var \MikoPBX\Common\Models\Fail2BanRules $res */
97
        $res = Fail2BanRules::findFirst("id = '1'");
98
        if ($res !== null) {
99
            $ban_time = $res->bantime;
100
        } else {
101
            $ban_time = '43800';
102
        }
103
        $path_db = self::FAIL2BAN_DB_PATH;
104
        $db      = new SQLite3($path_db);
105
        $db->busyTimeout(3000);
106
        if (false === $this->tableBanExists($db)) {
107
            return;
108
        }
109
        $q = 'DELETE' . ' from bans WHERE (timeofban+' . $ban_time . ')<' . time();
110
        $db->query($q);
111
    }
112
113
    /**
114
     * Checks whether BANS table exists in DB or not
115
     *
116
     * @param SQLite3 $db
117
     *
118
     * @return bool
119
     */
120
    public function tableBanExists($db): bool
121
    {
122
        $q_check      = 'SELECT name FROM sqlite_master WHERE type = "table" AND name="bans"';
123
        $result_check = $db->query($q_check);
124
125
        return (false !== $result_check && $result_check->fetchArray(SQLITE3_ASSOC) !== false);
126
    }
127
128
    /**
129
     * Shutdown fail2ban service
130
     */
131
    public function fail2banStop(): void
132
    {
133
        if (Util::isSystemctl()) {
134
            $systemctlPath = Util::which('systemctl');
135
            Util::mwExec("{$systemctlPath} stop fail2ban");
136
        } else {
137
            $fail2banPath = Util::which('fail2ban-client');
138
            Util::mwExec("{$fail2banPath} -x stop");
139
        }
140
    }
141
142
    /**
143
     * Create fail2ban dirs and DB if it does not exists
144
     *
145
     * @return string
146
     */
147
    public function fail2banMakeDirs(): string
148
    {
149
        $res_file = self::FAIL2BAN_DB_PATH;
150
        $filename = basename($res_file);
151
152
        $old_dir_db = '/cf/fail2ban';
153
        $dir_db     = $this->di->getShared('config')->path('core.fail2banDbDir');
154
        if (empty($dir_db)) {
155
            $dir_db = '/var/spool/fail2ban';
156
        }
157
        Util::mwMkdir($dir_db);
158
        // Создаем рабочие каталоги.
159
        $db_bd_dir = dirname($res_file);
160
        Util::mwMkdir($db_bd_dir);
161
162
        $create_link = false;
163
164
        // Символическая ссылка на базу данных.
165
        if (@filetype($res_file) !== 'link') {
166
            @unlink($res_file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

166
            /** @scrutinizer ignore-unhandled */ @unlink($res_file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
167
            $create_link = true;
168
        } elseif (readlink($res_file) === "$old_dir_db/$filename") {
169
            @unlink($res_file);
170
            $create_link = true;
171
            if (file_exists("$old_dir_db/$filename")) {
172
                // Перемещаем файл в новое местоположение.
173
                $mvPath = Util::which('mv');
174
                Util::mwExec("{$mvPath} '$old_dir_db/$filename' '$dir_db/$filename'");
175
            }
176
        }
177
178
        if ($create_link === true) {
179
            @symlink("$dir_db/$filename", $res_file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for symlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

179
            /** @scrutinizer ignore-unhandled */ @symlink("$dir_db/$filename", $res_file);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
180
        }
181
182
        return $res_file;
183
    }
184
185
    /**
186
     * Записываем конфиг для fail2ban. Описываем правила блокировок.
187
     */
188
    public function writeConfig(): void
189
    {
190
        $user_whitelist = '';
191
        /** @var \MikoPBX\Common\Models\Fail2BanRules $res */
192
        $res = Fail2BanRules::findFirst("id = '1'");
193
        if ($res !== null) {
194
            $max_retry     = $res->maxretry;
195
            $find_time     = $res->findtime;
196
            $ban_time      = $res->bantime;
197
            $whitelist     = $res->whitelist;
198
            $arr_whitelist = explode(' ', $whitelist);
199
            foreach ($arr_whitelist as $ip_string) {
200
                if (Verify::isIpAddress($ip_string)) {
201
                    $user_whitelist .= "$ip_string ";
202
                }
203
            }
204
            $net_filters = NetworkFilters::find("newer_block_ip = '1'");
205
            foreach ($net_filters as $filter) {
206
                $user_whitelist .= "{$filter->permit} ";
207
            }
208
209
            $user_whitelist = trim($user_whitelist);
210
        } else {
211
            $max_retry = '10';
212
            $find_time = '1800';
213
            $ban_time  = '43200';
214
        }
215
        $this->generateJails();
216
217
        $jails        = [
218
            'dropbear'    => 'iptables-allports[name=SSH, protocol=all]',
219
            'mikopbx-www' => 'iptables-allports[name=HTTP, protocol=all]',
220
        ];
221
        $modulesJails = $this->generateModulesJailsLocal();
222
        $jails        = array_merge($jails, $modulesJails);
223
        $config       = "[DEFAULT]\n" .
224
            "ignoreip = 127.0.0.1 {$user_whitelist}\n\n";
225
226
        $syslog_file = SyslogConf::getSyslogFile();
227
228
        foreach ($jails as $jail => $action) {
229
            $config .= "[{$jail}]\n" .
230
                "enabled = true\n" .
231
                "backend = process\n" .
232
                "logpath = {$syslog_file}\n" .
233
                // "logprocess = logread -f\n".
234
                "maxretry = {$max_retry}\n" .
235
                "findtime = {$find_time}\n" .
236
                "bantime = {$ban_time}\n" .
237
                "logencoding = utf-8\n" .
238
                "action = {$action}\n\n";
239
        }
240
241
        $log_dir = System::getLogDir() . '/asterisk/';
242
        $config  .= "[asterisk_security_log]\n" .
243
            "enabled = true\n" .
244
            "filter = asterisk\n" .
245
            "action = iptables-allports[name=ASTERISK, protocol=all]\n" .
246
            "logencoding = utf-8\n" .
247
            "maxretry = {$max_retry}\n" .
248
            "findtime = {$find_time}\n" .
249
            "bantime = {$ban_time}\n" .
250
            "logpath = {$log_dir}security_log\n\n";
251
252
        $config .= "[asterisk_error]\n" .
253
            "enabled = true\n" .
254
            "filter = asterisk\n" .
255
            "action = iptables-allports[name=ASTERISK_ERROR, protocol=all]\n" .
256
            "maxretry = {$max_retry}\n" .
257
            "findtime = {$find_time}\n" .
258
            "bantime = {$ban_time}\n" .
259
            "logencoding = utf-8\n" .
260
            "logpath = {$log_dir}error\n\n";
261
262
        $config .= "[asterisk_public]\n" .
263
            "enabled = true\n" .
264
            "filter = asterisk\n" .
265
            "action = iptables-allports[name=ASTERISK_PUBLIC, protocol=all]\n" .
266
            "maxretry = {$max_retry}\n" .
267
            "findtime = {$find_time}\n" .
268
            "bantime = {$ban_time}\n" .
269
            "logencoding = utf-8\n" .
270
            "logpath = {$log_dir}messages\n\n";
271
272
        Util::fileWriteContent('/etc/fail2ban/jail.local', $config);
273
    }
274
275
    /**
276
     * Creates additional rules
277
     */
278
    private function generateJails(): void
279
    {
280
        $filterPath = self::FILTER_PATH;
281
282
        $conf = "[INCLUDES]\n" .
283
            "before = common.conf\n" .
284
            "[Definition]\n" .
285
            "_daemon = [\S\W\s]+web_auth\n" .
286
            'failregex = ^%(__prefix_line)sFrom:\s<HOST>\sUserAgent:(\S|\s)*Wrong password$' . "\n" .
287
            '            ^(\S|\s)*nginx:\s+\d+/\d+/\d+\s+(\S|\s)*status\s+403(\S|\s)*client:\s+<HOST>(\S|\s)*' . "\n" .
288
            "ignoreregex =\n";
289
        file_put_contents("{$filterPath}/mikopbx-www.conf", $conf);
290
291
        $conf = "[INCLUDES]\n" .
292
            "before = common.conf\n" .
293
            "[Definition]\n" .
294
            "_daemon = (authpriv.warn )?dropbear\n" .
295
            'prefregex = ^%(__prefix_line)s<F-CONTENT>(?:[Ll]ogin|[Bb]ad|[Ee]xit).+</F-CONTENT>$' . "\n" .
296
            'failregex = ^[Ll]ogin attempt for nonexistent user (\'.*\' )?from <HOST>:\d+$' . "\n" .
297
            '            ^[Bb]ad (PAM )?password attempt for .+ from <HOST>(:\d+)?$' . "\n" .
298
            '            ^[Ee]xit before auth \(user \'.+\', \d+ fails\): Max auth tries reached - user \'.+\' from <HOST>:\d+\s*$' . "\n" .
299
            "ignoreregex =\n";
300
        file_put_contents("{$filterPath}/dropbear.conf", $conf);
301
302
        $this->generateModulesFilters();
303
    }
304
305
    /**
306
     * Generate additional modules filter files
307
     */
308
    protected function generateModulesFilters(): void
309
    {
310
        $filterPath        = self::FILTER_PATH;
311
        $additionalModules = $this->di->getShared('pbxConfModules');
312
        $rmPath            = Util::which('rm');
313
        Util::mwExec("{$rmPath} -rf {$filterPath}/module_*.conf");
314
        foreach ($additionalModules as $appClass) {
315
            if (method_exists($appClass, 'generateFail2BanJails')) {
316
                $content = $appClass->generateFail2BanJails();
317
                if ( ! empty($content)) {
318
                    $moduleUniqueId = $appClass->moduleUniqueId;
319
                    $fileName = Text::uncamelize($moduleUniqueId,'_').'.conf';
320
                    file_put_contents("{$filterPath}/{$fileName}", $content);
321
                }
322
            }
323
        }
324
    }
325
326
    /**
327
     * Generate additional modules include to /etc/fail2ban/jail.local
328
     *
329
     * @return array
330
     */
331
    protected function generateModulesJailsLocal(): array
332
    {
333
        $jails             = [];
334
        $additionalModules = $this->di->getShared('pbxConfModules');
335
        foreach ($additionalModules as $appClass) {
336
            if (method_exists($appClass, 'generateFail2BanJails')) {
337
                $content = $appClass->generateFail2BanJails();
338
                if ( ! empty($content)) {
339
                    $moduleUniqueId                    = $appClass->moduleUniqueId;
340
                    $fileName = Text::uncamelize($moduleUniqueId,'_');
341
                    $jails[$fileName] = "iptables-allports[name={$moduleUniqueId}, protocol=all]";
342
                }
343
            }
344
        }
345
346
        return $jails;
347
    }
348
349
}